Agent:
  name: Liti-agent_Claim_Identification
  description: 청구권 식별 - Fact Ledger + BO → 청구권 식별 뷰 생성 → LLM 청구권 추론
  version: 1.1
  Stages:
  - name: stage_청구권식별
    description: Fact_Ledger + BO → claim_identification_view → claims_identified → claims_identified_case_type
    llm_provider: openai
    llm_model: gpt-4o-2024-08-06
    tools:
      mcpServers:
        localdocs:
          type: streamable-http
          url: http://mcp-localdocs:8012/mcp
          description: Get the content of local documents
        code-executor:
          type: streamable-http
          url: https://code-executor.mcp.eroomai.com/mcp
          description: Run scripts of programming languages
          headers:
            Authorization: Bearer rR8OXqWrVZA1gFEo8oWfBkw2XgpWoGrrspw5ObsxTCM=
    prevs: []
    nexts: []
    task_procedure:
      IN:
        nexts:
        - Task_A
        wait_until: []
      Task_A:
        nexts:
        - Task_B
        wait_until:
        - IN
      Task_B:
        nexts:
        - Task_C
        wait_until:
        - Task_A
      Task_C:
        nexts:
        - OUT
        wait_until:
        - Task_B
      OUT:
        nexts: []
        wait_until:
        - Task_C
    tasks:
    - task_name: Task_A
      mcp: code-executor
      tool_name: run_code
      parameters:
        language: python
        requirements: httpx
        network: agent-network
        timeout: 180
        code: |
          #!/usr/bin/env python3
          from __future__ import annotations

          import json
          import re
          import sys
          from typing import Any

          import httpx

          # =============================================================================
          # Task_A v2 (semantic-projection extended)
          # - preserves BO.Action label information
          # - preserves party directionality (Performer/Subject + claim roles)
          # - backfills enriched legal_calculation_object fields from linked evidence
          # - projects evidence_event_candidates into claim-identification view
          # - projects contract_components / valuation_object / state metadata
          # - adds cluster_context / validation_warnings / legacy keyword bridge
          # - optionally joins actio-pauliana support artifacts from stage1 outputs
          # =============================================================================

          LOCALDOCS_URL = "http://mcp-localdocs:8012/mcp"
          MCP_HEADERS = {
              "Content-Type": "application/json",
              "Accept": "application/json, text/event-stream",
          }
          CLIENT = httpx.Client(timeout=60)

          # downstream Task_B compatibility를 위해 schema_version은 유지한다.
          # additive field만 확장한다.
          SCHEMA_VERSION = "claim_identification_view.v2"

          LEGAL_CALC_SCALAR_FIELDS = [
              "asset_id",
              "property_label_for_schedule",
              "transaction_type",
              "transaction_date",
              "registration_date",
              "registry_office",
              "registry_receipt_no",
              "registry_recorded_transfer_date",
              "market_value_at_act",
              "market_value_at_close",
              "sale_price",
              "claim_id",
              "claim_label_for_schedule",
              "claim_event_type",
              "claim_creditor",
              "claim_debtor",
              "claim_guarantor",
              "claim_security_provider",
              "claim_arising_date",
              "claim_maturity_date",
              "claim_default_or_acceleration_date",
              "claim_performance_date",
              "claim_instrument_date",
              "claim_instrument_identifier",
              "claim_instrument_issuer",
              "claim_principal_amount_at_act",
              "claim_total_amount_at_act",
              "claim_outstanding_amount_at_close",
              "claim_actual_performance_amount",
              "claim_guarantee_limit_amount",
              "claim_secured_cap_amount",
              "claim_interest_rate",
              "claim_default_rate",
          ]

          LEGAL_CALC_ARRAY_FIELDS = [
              "consideration_breakdown",
              "claim_nature_tags",
              "claim_component_breakdown",
              "claim_status_tags",
          ]

          SEMANTIC_DOC_FLAGS = {
              "multi_event_doc",
              "registry_doc",
              "mixed_contract_doc",
              "valuation_doc",
              "claim_doc",
              "status_doc",
              "notice_doc",
              "response_doc",
              "business_transfer_doc",
              "supporting_reference_doc",
          }

          STATE_STATUS_TAGS = {
              "ongoing",
              "current_possession",
              "current_use",
              "current_control",
          }

          BRIDGEABLE_COMPONENT_KINDS = {
              "lease",
              "sale",
              "building_sale",
              "delivery",
              "management_use_shift",
              "permit_cooperation",
              "business_transfer",
          }

          EVENT_KIND_TO_KEYWORDS = {
              "임대차": ["임대차"],
              "점유": ["점유", "현재점유"],
              "인도": ["인도"],
              "경매신청": ["경매신청"],
              "경매개시": ["경매개시", "경매개시결정"],
              "경매매각": ["경매매각", "매각", "경매"],
              "소유권이전": ["소유권이전"],
              "증여": ["증여"],
              "대물변제": ["대물변제"],
              "담보설정": ["근저당", "담보설정"],
              "담보말소": ["근저당말소", "담보말소"],
              "신축": ["신축"],
              "사업자등록": ["사업자등록", "영업"],
              "통지": ["통지", "최고"],
              "해제/해지": ["해제", "해지"],
          }

          POSITIVE_CORROBORATION_VALUES = {
              "보강존재",
              "복수",
              "다수",
              "2개이상",
              "2건이상",
              "있음",
              "존재",
              "multiple",
              "corroborated",
          }
          NEGATIVE_CORROBORATION_VALUES = {
              "단독",
              "단일",
              "보강없음",
              "없음",
              "없다",
              "single",
              "none",
          }
          EVIDENCE_INDEX_PATTERN = re.compile(r"\bE-\d{3}\b")


          # =============================================================================
          # MCP helpers
          # =============================================================================

          def _init_mcp_session() -> None:
              response = CLIENT.post(
                  LOCALDOCS_URL,
                  json={
                      "jsonrpc": "2.0",
                      "id": 1,
                      "method": "initialize",
                      "params": {
                          "protocolVersion": "2025-03-26",
                          "capabilities": {},
                          "clientInfo": {
                              "name": "claim-identification-task-a-v2-semantic-projection",
                              "version": "1.0",
                              "user_id": "{{__user_hash__}}",
                              "workspace_id": "{{__workspace_hash__}}",
                          },
                      },
                  },
                  headers=MCP_HEADERS,
              )
              response.raise_for_status()
              session_id = response.headers.get("mcp-session-id")
              if session_id:
                  MCP_HEADERS["mcp-session-id"] = session_id

              CLIENT.post(
                  LOCALDOCS_URL,
                  json={"jsonrpc": "2.0", "method": "notifications/initialized"},
                  headers=MCP_HEADERS,
              ).raise_for_status()


          def _parse_mcp_response(text: str) -> dict[str, Any] | None:
              for line in text.strip().split("\n"):
                  if line.startswith("data: "):
                      payload = line[6:]
                      try:
                          return json.loads(payload)
                      except Exception:
                          return None
              try:
                  return json.loads(text)
              except Exception:
                  return None


          def _call_tool(name: str, arguments: dict[str, Any], msg_id: int) -> dict[str, Any]:
              response = CLIENT.post(
                  LOCALDOCS_URL,
                  json={
                      "jsonrpc": "2.0",
                      "id": msg_id,
                      "method": "tools/call",
                      "params": {"name": name, "arguments": arguments},
                  },
                  headers=MCP_HEADERS,
              )
              response.raise_for_status()
              parsed = _parse_mcp_response(response.text)
              if not parsed or "result" not in parsed:
                  raise RuntimeError(f"MCP tool call failed: {name}: {response.text[:500]}")
              return parsed


          def _extract_json_from_read_docs_result(result: dict[str, Any], doc_name: str) -> Any:
              content = result["result"].get("content", [])
              text = content[0].get("text", "") if content else ""

              try:
                  parsed = json.loads(text)
              except json.JSONDecodeError as exc:
                  raise RuntimeError(f"Invalid read_docs JSON envelope for {doc_name}: {text[:300]}") from exc

              if isinstance(parsed, dict) and "results" in parsed:
                  results = parsed.get("results") or []
                  if not results:
                      raise RuntimeError(f"No read_docs results for {doc_name}")
                  inner = results[0].get("content", "")
                  if not inner:
                      raise RuntimeError(f"Empty content for {doc_name}")
                  if isinstance(inner, str):
                      try:
                          return json.loads(inner)
                      except json.JSONDecodeError:
                          # Extra data 대응: 첫 번째 JSON 객체만 파싱
                          try:
                              obj, _ = json.JSONDecoder().raw_decode(inner.strip())
                              return obj
                          except json.JSONDecodeError as exc:
                              raise RuntimeError(f"Content of {doc_name} is not valid JSON: {inner[:300]}") from exc
                  return inner

              return parsed


          def _read_required_json_doc(doc_name: str, msg_id: int) -> Any:
              result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=msg_id)
              return _extract_json_from_read_docs_result(result, doc_name)


          def _read_optional_json_doc(doc_name: str, msg_id: int) -> Any | None:
              try:
                  result = _call_tool("read_docs", {"doc_names": [doc_name]}, msg_id=msg_id)
                  return _extract_json_from_read_docs_result(result, doc_name)
              except Exception as exc:
                  print(f"[Task_A] optional source skipped: {doc_name}: {exc}", file=sys.stderr)
                  return None


          def _write_doc(doc_name: str, content: str, msg_id: int) -> None:
              _call_tool("write_file", {"path": doc_name, "content": content, "overwrite": True}, msg_id=msg_id)



          # =============================================================================
          # Payload unwrappers
          # =============================================================================

          def _unwrap_evidence_indexed_payload(payload: Any) -> list[dict[str, Any]] | None:
              if isinstance(payload, list):
                  return [item for item in payload if isinstance(item, dict)]
              if isinstance(payload, dict):
                  items = payload.get("items")
                  if isinstance(items, list):
                      return [item for item in items if isinstance(item, dict)]
                  docs = payload.get("documents")
                  if isinstance(docs, list):
                      return [item for item in docs if isinstance(item, dict)]
              return None


          def _unwrap_event_candidates_payload(payload: Any) -> dict[str, Any] | None:
              if isinstance(payload, dict):
                  if isinstance(payload.get("items"), list):
                      return payload
                  inner = payload.get("evidence_event_candidates")
                  if isinstance(inner, dict) and isinstance(inner.get("items"), list):
                      return inner
                  docs = payload.get("documents")
                  if isinstance(docs, list):
                      if len(docs) == 1 and isinstance(docs[0], dict) and isinstance(docs[0].get("items"), list):
                          return docs[0]
                      return {"items": [item for item in docs if isinstance(item, dict)]}
              if isinstance(payload, list):
                  return {"items": [item for item in payload if isinstance(item, dict)]}
              return None


          # =============================================================================
          # Generic helpers
          # =============================================================================

          def _coerce_str(value: Any) -> str | None:
              if value is None:
                  return None
              text = str(value).strip()
              return text or None


          def _has_value(value: Any) -> bool:
              if value is None:
                  return False
              if isinstance(value, str):
                  return bool(value.strip())
              if isinstance(value, list):
                  return any(_has_value(item) for item in value)
              if isinstance(value, dict):
                  return bool(value)
              return True


          def _coerce_list(value: Any) -> list[str]:
              if not isinstance(value, list):
                  return []
              out: list[str] = []
              for item in value:
                  text = _coerce_str(item)
                  if text:
                      out.append(text)
              return out


          def _coerce_dict(value: Any) -> dict[str, Any]:
              return value if isinstance(value, dict) else {}


          def _coerce_obj_list(value: Any) -> list[dict[str, Any]]:
              if not isinstance(value, list):
                  return []
              return [item for item in value if isinstance(item, dict)]


          def _uniq(values: list[Any]) -> list[Any]:
              seen: set[str] = set()
              out: list[Any] = []
              for value in values:
                  if value is None:
                      continue
                  if isinstance(value, str):
                      value = value.strip()
                      if not value:
                          continue
                  key = json.dumps(value, ensure_ascii=False, sort_keys=True) if isinstance(value, (dict, list)) else str(value)
                  if key in seen:
                      continue
                  seen.add(key)
                  out.append(value)
              return out


          def _normalize_array(value: Any) -> list[str]:
              return _uniq(_coerce_list(value))


          def _normalize_keyword_list(values: list[Any]) -> list[str]:
              out: list[str] = []
              for value in values:
                  text = _coerce_str(value)
                  if not text:
                      continue
                  text = re.sub(r"\s+", " ", text).strip()
                  if text:
                      out.append(text)
              return _uniq(out)


          def _first_non_empty(*values: Any) -> Any:
              for value in values:
                  if _has_value(value):
                      if isinstance(value, str):
                          return value.strip()
                      return value
              return None


          def _strip_event_class(value: Any) -> str | None:
              text = _coerce_str(value)
              if not text:
                  return None
              return text.split("(")[0].strip() or None


          def _summarize_unique(values: list[Any], mixed_label: str = "혼재") -> str | None:
              normalized = _uniq([_coerce_str(value) for value in values if _coerce_str(value)])
              if not normalized:
                  return None
              if len(normalized) == 1:
                  return normalized[0]
              return mixed_label


          def _normalize_date_text(value: Any) -> str | None:
              text = _coerce_str(value)
              if not text:
                  return None
              return re.sub(r"[.\s]+", "-", text).strip("-")


          def _contains_any(texts: list[str], keywords: list[str]) -> bool:
              return any(k in text for text in texts for k in keywords if text)


          def _safe_excerpt(value: Any, limit: int = 80) -> str | None:
              text = _coerce_str(value)
              if not text:
                  return None
              text = re.sub(r"\s+", " ", text).strip()
              return text[:limit]


          # =============================================================================
          # Action / event helpers
          # =============================================================================

          def _extract_colon_label(action_text: Any) -> str | None:
              text = _coerce_str(action_text)
              if not text or ":" not in text:
                  return None
              label = text.split(":", 1)[0].strip()
              return label or None


          def _extract_action_label(bo: dict[str, Any]) -> str | None:
              juristic_act = bo.get("JuristicAct") or {}
              juristic_label = juristic_act.get("label") if isinstance(juristic_act, dict) else None
              return _first_non_empty(
                  _extract_colon_label(bo.get("Action")),
                  juristic_label,
              )


          def _infer_claim_event_type(merged_lco: dict[str, Any], bo: dict[str, Any]) -> str | None:
              explicit = _coerce_str(merged_lco.get("claim_event_type"))
              if explicit:
                  return explicit

              action_label = _coerce_str(_extract_action_label(bo))
              juristic_act = bo.get("JuristicAct") or {}
              juristic_label = _coerce_str(juristic_act.get("label") if isinstance(juristic_act, dict) else None)
              legal_keywords = _coerce_list(bo.get("Legal_Keywords"))
              tokens = _uniq([action_label, juristic_label] + legal_keywords)

              def any_token_contains(keyword: str) -> bool:
                  return any(keyword in token for token in tokens if token)

              def any_token_exact(word: str) -> bool:
                  return any(token == word for token in tokens if token)

              if any_token_contains("신용보증"):
                  return "신용보증"
              if any_token_contains("연대보증"):
                  return "연대보증"
              if action_label == "보증" or juristic_label == "보증" or any_token_contains("보증채무"):
                  return "보증채무"
              if any_token_contains("대위변제"):
                  return "대위변제"
              if any_token_contains("구상권발생") or any_token_contains("구상금"):
                  return "구상권발생"
              if action_label == "변제" or juristic_label == "변제" or any_token_exact("변제"):
                  return "변제"
              if any_token_contains("배당"):
                  return "배당"
              if any_token_contains("채권양도"):
                  return "채권양도"
              if any_token_contains("채무인수"):
                  return "채무인수"
              if any_token_contains("상계"):
                  return "상계"
              if any_token_contains("면제"):
                  return "면제"
              if any_token_contains("경개"):
                  return "경개"
              if any_token_contains("근저당") or any_token_contains("질권") or any_token_contains("담보제공"):
                  return "담보제공"
              if any_token_contains("소비대차") or any_token_contains("대여") or any_token_contains("대출") or any_token_contains("차용"):
                  return "대여"
              return None


          # =============================================================================
          # Evidence helpers
          # =============================================================================

          def _parse_evidence_indexes_from_refs(evidence_refs: list[str]) -> list[str]:
              out: list[str] = []
              for ref in evidence_refs or []:
                  if ref:
                      out.extend(EVIDENCE_INDEX_PATTERN.findall(str(ref)))
              return _uniq(out)


          def _normalize_evidence_refs(fact_refs: Any, bo_evidence: list[dict[str, Any]]) -> list[str]:
              refs = [
                  _coerce_str(ref)
                  for ref in (fact_refs or [])
                  if _coerce_str(ref) and _coerce_str(ref) != "증거공백"
              ]
              refs = [ref for ref in refs if ref]
              if refs:
                  return _uniq(refs)

              synthesized: list[str] = []
              for evidence in bo_evidence or []:
                  evidence_index = _coerce_str(evidence.get("evidence_index"))
                  source_title = _coerce_str(evidence.get("source_title"))
                  if evidence_index and source_title:
                      synthesized.append(f"{evidence_index} ({source_title})")
                  elif evidence_index:
                      synthesized.append(evidence_index)
              return _uniq(synthesized)


          def _extract_evidence_titles(evidence_refs: list[str], bo_evidence: list[dict[str, Any]]) -> list[str]:
              titles = _uniq([
                  _coerce_str(evidence.get("source_title"))
                  for evidence in bo_evidence or []
                  if _coerce_str(evidence.get("source_title"))
              ])
              if titles:
                  return titles

              parsed_titles: list[str] = []
              for ref in evidence_refs or []:
                  text = _coerce_str(ref)
                  if not text:
                      continue
                  match = re.match(r"^E-\d{3}\s+\((.*)\)$", text)
                  if match:
                      parsed_titles.append(match.group(1).strip())
              return _uniq(parsed_titles)


          def _corroboration_to_bool(value: Any) -> bool | None:
              text = _coerce_str(value)
              if not text:
                  return None
              if text in POSITIVE_CORROBORATION_VALUES:
                  return True
              if text in NEGATIVE_CORROBORATION_VALUES:
                  return False
              return None


          def _summarize_content_relevance(values: list[Any]) -> str | None:
              normalized = _uniq([_coerce_str(value) for value in values if _coerce_str(value)])
              if not normalized:
                  return None
              if len(normalized) == 1:
                  return normalized[0]
              if "직접" in normalized:
                  return "직접포함"
              return "혼재"


          def _build_evidence_strength(bo_evidence: list[dict[str, Any]], fallback_indexes: list[str]) -> dict[str, Any]:
              bo_evidence = bo_evidence or []
              evidence_indexes = _uniq(
                  [
                      _coerce_str(evidence.get("evidence_index"))
                      for evidence in bo_evidence
                      if _coerce_str(evidence.get("evidence_index"))
                  ]
                  + list(fallback_indexes or [])
              )
              raw_corroboration_values = _uniq([
                  _coerce_str(evidence.get("corroboration"))
                  for evidence in bo_evidence
                  if _coerce_str(evidence.get("corroboration"))
              ])
              corroboration_votes = [_corroboration_to_bool(value) for value in raw_corroboration_values]

              if any(v is True for v in corroboration_votes) or len(evidence_indexes) >= 2:
                  has_corroboration: bool | None = True
              elif bo_evidence or evidence_indexes:
                  if corroboration_votes and all(v is False for v in corroboration_votes if v is not None) and len(evidence_indexes) < 2:
                      has_corroboration = False
                  elif len(evidence_indexes) == 1:
                      has_corroboration = False
                  else:
                      has_corroboration = None
              else:
                  has_corroboration = None

              return {
                  "evidence_count": len(bo_evidence) if bo_evidence else (len(evidence_indexes) if evidence_indexes else 0),
                  "evidence_index_count": len(evidence_indexes),
                  "evidence_indexes": evidence_indexes,
                  "has_direct_evidence": any(_coerce_str(evidence.get("content_relevance")) == "직접" for evidence in bo_evidence) if bo_evidence else None,
                  "has_indirect_evidence": any(_coerce_str(evidence.get("content_relevance")) == "간접" for evidence in bo_evidence) if bo_evidence else None,
                  "has_contrary_evidence": any(_coerce_str(evidence.get("content_relevance")) == "반대" for evidence in bo_evidence) if bo_evidence else None,
                  "has_corroboration": has_corroboration,
                  "corroboration_summary": _summarize_unique(raw_corroboration_values),
                  "authentication_status_summary": _summarize_unique([evidence.get("authentication_status") for evidence in bo_evidence]),
                  "authentication_statuses": _uniq([
                      _coerce_str(evidence.get("authentication_status"))
                      for evidence in bo_evidence
                      if _coerce_str(evidence.get("authentication_status"))
                  ]),
                  "content_relevance_summary": _summarize_content_relevance([evidence.get("content_relevance") for evidence in bo_evidence]),
              }


          # =============================================================================
          # legal_calculation_object merge helpers
          # =============================================================================

          def _merge_legal_calculation_object(
              fact_legal_calc: dict[str, Any] | None,
              linked_evidence_legal_calcs: list[dict[str, Any]],
          ) -> tuple[dict[str, Any], dict[str, str], dict[str, Any]]:
              fact_legal_calc = fact_legal_calc if isinstance(fact_legal_calc, dict) else {}
              linked_evidence_legal_calcs = [obj for obj in linked_evidence_legal_calcs if isinstance(obj, dict)]

              merged: dict[str, Any] = {}
              field_sources: dict[str, str] = {}
              conflicts: dict[str, Any] = {}

              for field in LEGAL_CALC_SCALAR_FIELDS:
                  fact_value = _coerce_str(fact_legal_calc.get(field))
                  evidence_values = _uniq([
                      _coerce_str(obj.get(field))
                      for obj in linked_evidence_legal_calcs
                      if _coerce_str(obj.get(field))
                  ])

                  if fact_value:
                      merged[field] = fact_value
                      field_sources[field] = "fact_ledger"
                      if evidence_values and any(value != fact_value for value in evidence_values):
                          conflicts[field] = {
                              "fact_ledger": fact_value,
                              "linked_evidence": evidence_values,
                          }
                  elif evidence_values:
                      merged[field] = evidence_values[0]
                      field_sources[field] = "linked_evidence"
                      if len(evidence_values) > 1:
                          conflicts[field] = {"linked_evidence": evidence_values}
                  else:
                      merged[field] = None

              for field in LEGAL_CALC_ARRAY_FIELDS:
                  fact_values = _normalize_array(fact_legal_calc.get(field))
                  evidence_values = _uniq([
                      item
                      for obj in linked_evidence_legal_calcs
                      for item in _normalize_array(obj.get(field))
                  ])

                  if fact_values and evidence_values:
                      merged[field] = _uniq(fact_values + evidence_values)
                      field_sources[field] = "fact_ledger+linked_evidence"
                      extra_values = [value for value in evidence_values if value not in fact_values]
                      if extra_values:
                          conflicts[field] = {
                              "fact_ledger": fact_values,
                              "linked_evidence_extra": extra_values,
                          }
                  elif fact_values:
                      merged[field] = fact_values
                      field_sources[field] = "fact_ledger"
                  elif evidence_values:
                      merged[field] = evidence_values
                      field_sources[field] = "linked_evidence"
                  else:
                      merged[field] = []

              return merged, field_sources, conflicts


          def _derive_amount_and_source_field(fact_amount: Any, merged_lco: dict[str, Any]) -> tuple[str | None, str | None]:
              candidates = [
                  ("fact.amount", _coerce_str(fact_amount)),
                  ("claim_actual_performance_amount", _coerce_str(merged_lco.get("claim_actual_performance_amount"))),
                  ("claim_outstanding_amount_at_close", _coerce_str(merged_lco.get("claim_outstanding_amount_at_close"))),
                  ("claim_total_amount_at_act", _coerce_str(merged_lco.get("claim_total_amount_at_act"))),
                  ("claim_principal_amount_at_act", _coerce_str(merged_lco.get("claim_principal_amount_at_act"))),
                  ("claim_guarantee_limit_amount", _coerce_str(merged_lco.get("claim_guarantee_limit_amount"))),
                  ("claim_secured_cap_amount", _coerce_str(merged_lco.get("claim_secured_cap_amount"))),
                  ("sale_price", _coerce_str(merged_lco.get("sale_price"))),
                  ("market_value_at_act", _coerce_str(merged_lco.get("market_value_at_act"))),
                  ("market_value_at_close", _coerce_str(merged_lco.get("market_value_at_close"))),
              ]
              for field_name, value in candidates:
                  if value:
                      return value, field_name
              return None, None


          # =============================================================================
          # Evidence semantic projection helpers
          # =============================================================================

          def _project_contract_components(
              evidence_indexes: list[str],
              evidence_indexed_by_id: dict[str, dict[str, Any]],
          ) -> tuple[list[dict[str, Any]], list[str]]:
              projected: list[dict[str, Any]] = []
              source_indexes: list[str] = []

              for evidence_index in evidence_indexes:
                  record = _coerce_dict(evidence_indexed_by_id.get(evidence_index))
                  components = _coerce_obj_list(record.get("contract_components"))
                  if not components:
                      continue
                  source_indexes.append(evidence_index)
                  for comp in components:
                      projected.append({
                          "evidence_index": evidence_index,
                          "component_id": _coerce_str(comp.get("component_id")),
                          "component_kind": _coerce_str(comp.get("component_kind")),
                          "object_spec": _coerce_str(comp.get("object_spec")),
                          "amount": _coerce_str(comp.get("amount")),
                          "date": _coerce_str(comp.get("date")),
                          "source_locator": _coerce_dict(comp.get("source_locator")),
                      })

              return _uniq(projected), _uniq(source_indexes)


          def _project_valuation_contexts(
              evidence_indexes: list[str],
              evidence_indexed_by_id: dict[str, dict[str, Any]],
          ) -> tuple[list[dict[str, Any]], list[str]]:
              projected: list[dict[str, Any]] = []
              source_indexes: list[str] = []

              for evidence_index in evidence_indexes:
                  record = _coerce_dict(evidence_indexed_by_id.get(evidence_index))
                  valuation = _coerce_dict(record.get("valuation_object"))
                  if not valuation:
                      continue
                  source_indexes.append(evidence_index)
                  projected.append({
                      "evidence_index": evidence_index,
                      "valuation_kind": _coerce_str(valuation.get("valuation_kind")),
                      "target_object": _coerce_str(valuation.get("target_object")),
                      "basis_date_or_period": _coerce_str(valuation.get("basis_date_or_period")),
                      "value_rows": [
                          {
                              "label": _coerce_str(row.get("label")),
                              "amount": _coerce_str(row.get("amount")),
                              "unit": _coerce_str(row.get("unit")),
                          }
                          for row in _coerce_obj_list(valuation.get("value_rows"))
                      ],
                  })

              return _uniq(projected), _uniq(source_indexes)


          def _build_evidence_semantics(
              evidence_indexes: list[str],
              evidence_indexed_by_id: dict[str, dict[str, Any]],
          ) -> dict[str, Any]:
              property_cluster_ids: list[str] = []
              doc_semantic_flags: list[str] = []
              multi_event_registry_indexes: list[str] = []

              for evidence_index in evidence_indexes:
                  record = _coerce_dict(evidence_indexed_by_id.get(evidence_index))
                  cluster_id = _coerce_str(record.get("property_cluster_id"))
                  if cluster_id:
                      property_cluster_ids.append(cluster_id)

                  for flag in _coerce_list(record.get("doc_semantic_flags")):
                      if flag in SEMANTIC_DOC_FLAGS:
                          doc_semantic_flags.append(flag)

                  if record.get("multi_event_registry") is True:
                      multi_event_registry_indexes.append(evidence_index)

              contract_components, contract_source_indexes = _project_contract_components(
                  evidence_indexes, evidence_indexed_by_id
              )
              valuation_contexts, valuation_source_indexes = _project_valuation_contexts(
                  evidence_indexes, evidence_indexed_by_id
              )

              return {
                  "property_cluster_ids": _uniq(property_cluster_ids),
                  "doc_semantic_flags": _uniq(doc_semantic_flags),
                  "multi_event_registry_indexes": _uniq(multi_event_registry_indexes),
                  "contract_components": contract_components,
                  "contract_component_source_indexes": contract_source_indexes,
                  "valuation_contexts": valuation_contexts,
                  "valuation_source_indexes": valuation_source_indexes,
              }


          # =============================================================================
          # Event candidate projection helpers
          # =============================================================================

          def _candidate_overlap_score(
              fact: dict[str, Any],
              bo: dict[str, Any],
              candidate: dict[str, Any],
          ) -> int:
              score = 0
              fact_date = _coerce_str(fact.get("date"))
              bo_date = _coerce_str(bo.get("BehaviorTime"))
              cand_date = _coerce_str(candidate.get("event_date"))
              if cand_date and cand_date in {fact_date, bo_date}:
                  score += 4

              party_pool = set(_coerce_list(fact.get("parties")))
              performer = _coerce_str(bo.get("Performer"))
              subject = _coerce_str(bo.get("Subject"))
              if performer:
                  party_pool.add(performer)
              if subject:
                  party_pool.add(subject)

              participants = _coerce_dict(candidate.get("participants"))
              candidate_parties = (
                  _coerce_list(participants.get("actor_candidates"))
                  + _coerce_list(participants.get("counterparty_candidates"))
                  + _coerce_list(participants.get("beneficiary_candidates"))
                  + _coerce_list(participants.get("third_party_candidates"))
              )
              score += 2 * len([p for p in candidate_parties if p in party_pool])

              fact_objects = [
                  _coerce_str(fact.get("object_spec")) or "",
                  _coerce_str(bo.get("Object")) or "",
              ]
              cand_object = _coerce_str(candidate.get("object_spec")) or ""
              if cand_object and any(cand_object in obj or obj in cand_object for obj in fact_objects if obj):
                  score += 3

              action_texts = [
                  _coerce_str(fact.get("action")) or "",
                  _coerce_str(bo.get("Action")) or "",
              ]
              cand_kind = _coerce_str(candidate.get("event_kind")) or ""
              keywords = EVENT_KIND_TO_KEYWORDS.get(cand_kind, [])
              if _contains_any(action_texts, keywords):
                  score += 2

              if candidate.get("event_or_state") == "state":
                  status_tags = set(_coerce_list(bo.get("StatusTags")))
                  if status_tags.intersection(STATE_STATUS_TAGS):
                      score += 2
              return score


          def _project_linked_event_candidates(
              fact: dict[str, Any],
              bo: dict[str, Any],
              evidence_indexes: list[str],
              event_candidates_by_index: dict[str, dict[str, Any]],
              limit: int = 5,
          ) -> tuple[list[dict[str, Any]], list[str]]:
              scored: list[tuple[int, dict[str, Any], str]] = []

              for evidence_index in evidence_indexes:
                  doc = _coerce_dict(event_candidates_by_index.get(evidence_index))
                  candidates = _coerce_obj_list(doc.get("event_candidates"))
                  for candidate in candidates:
                      score = _candidate_overlap_score(fact, bo, candidate)
                      if score <= 0:
                          continue
                      scored.append((score, candidate, evidence_index))

              scored.sort(
                  key=lambda x: (
                      -x[0],
                      _coerce_str(x[1].get("event_date")) or "9999-99-99",
                      _coerce_str(x[1].get("candidate_id")) or "",
                  )
              )

              out: list[dict[str, Any]] = []
              source_indexes: list[str] = []
              for _, candidate, evidence_index in scored[:limit]:
                  source_indexes.append(evidence_index)
                  out.append({
                      "evidence_index": evidence_index,
                      "candidate_id": _coerce_str(candidate.get("candidate_id")),
                      "event_or_state": _coerce_str(candidate.get("event_or_state")),
                      "event_kind": _coerce_str(candidate.get("event_kind")),
                      "event_subkind": _coerce_str(candidate.get("event_subkind")),
                      "action_summary": _coerce_str(candidate.get("action_summary")),
                      "object_spec": _coerce_str(candidate.get("object_spec")),
                      "amount": _coerce_str(candidate.get("amount")),
                      "event_date": _coerce_str(candidate.get("event_date")),
                      "identity_signature": _coerce_str(candidate.get("identity_signature")),
                      "participants": _coerce_dict(candidate.get("participants")),
                      "current_state_candidate": _coerce_dict(candidate.get("current_state_candidate")),
                  })

              return _uniq(out), _uniq(source_indexes)


          # =============================================================================
          # Actio-pauliana support merge helpers
          # =============================================================================

          def _merge_actio_support(
              source_bo_id: str | None,
              evidence_indexes: list[str],
              fact_actio_by_bo: dict[str, dict[str, Any]],
              evidence_actio_by_index: dict[str, dict[str, Any]],
              actio_case_signals: dict[str, Any] | None,
          ) -> dict[str, Any] | None:
              support_records: list[tuple[str, str, dict[str, Any]]] = []
              matched_evidence_indexes: list[str] = []

              if source_bo_id:
                  fact_support_record = fact_actio_by_bo.get(source_bo_id)
                  if isinstance(fact_support_record, dict) and isinstance(fact_support_record.get("actio_pauliana_support"), dict):
                      support_records.append(("fact", source_bo_id, fact_support_record["actio_pauliana_support"]))

              for evidence_index in evidence_indexes:
                  evidence_support_record = evidence_actio_by_index.get(evidence_index)
                  if isinstance(evidence_support_record, dict) and isinstance(evidence_support_record.get("actio_pauliana_support"), dict):
                      support_records.append(("evidence", evidence_index, evidence_support_record["actio_pauliana_support"]))
                      matched_evidence_indexes.append(evidence_index)

              if not support_records:
                  return None

              role_tags: list[str] = []
              relevance_votes: list[bool] = []
              confidence_values: list[str] = []

              for _, _, support in support_records:
                  role_tags.extend(_coerce_list(support.get("support_role_tags")))
                  meta = support.get("legal_meta_flags") or {}
                  if isinstance(meta, dict):
                      if meta.get("is_actio_pauliana_relevant_candidate") is True:
                          relevance_votes.append(True)
                      confidence_score = _coerce_str(meta.get("confidence_score"))
                      if confidence_score:
                          confidence_values.append(confidence_score)

              meta_list = [(support.get("legal_meta_flags") or {}) for _, _, support in support_records]

              merged = {
                  "is_actio_pauliana_relevant_candidate": True if relevance_votes else None,
                  "support_role_tags": _uniq(role_tags),
                  "fraudulent_act_date": _first_non_empty(*[support.get("fraudulent_act_date") for _, _, support in support_records]),
                  "restoration_mode_candidate": _first_non_empty(*[
                      meta.get("restoration_mode_candidate") if isinstance(meta, dict) else None
                      for meta in meta_list
                  ]),
                  "beneficiary_gain_support": _first_non_empty(*[support.get("beneficiary_gain_support") for _, _, support in support_records]),
                  "lease_deposit_deductibility_support": _first_non_empty(*[support.get("lease_deposit_deductibility_support") for _, _, support in support_records]),
                  "non_deductible_attachment_claims": _first_non_empty(*[support.get("non_deductible_attachment_claims") for _, _, support in support_records]),
                  "encumbrances_at_fraudulent_act": _first_non_empty(*[support.get("encumbrances_at_fraudulent_act") for _, _, support in support_records]),
                  "encumbrances_at_close_of_arguments": _first_non_empty(*[support.get("encumbrances_at_close_of_arguments") for _, _, support in support_records]),
                  "plaintiff_claim_snapshots": _first_non_empty(*[support.get("plaintiff_claim_snapshots") for _, _, support in support_records]),
                  "support_evidence_indexes": _uniq(matched_evidence_indexes),
                  "confidence_score_summary": _summarize_unique(confidence_values),
              }

              if isinstance(actio_case_signals, dict):
                  related_bo_ids = set(_coerce_list(actio_case_signals.get("related_bo_ids")))
                  related_evidence_indexes = set(_coerce_list(actio_case_signals.get("related_evidence_indexes")))
                  if (source_bo_id and source_bo_id in related_bo_ids) or related_evidence_indexes.intersection(set(evidence_indexes)):
                      merged["case_signal_match"] = {
                          "suspicion_level": actio_case_signals.get("suspicion_level"),
                          "target_property_candidates": actio_case_signals.get("target_property_candidates") or [],
                          "fraudulent_act_date_candidates": actio_case_signals.get("fraudulent_act_date_candidates") or [],
                          "preserved_claim_candidates": actio_case_signals.get("preserved_claim_candidates") or [],
                          "beneficiary_candidates": actio_case_signals.get("beneficiary_candidates") or [],
                      }

              cleaned: dict[str, Any] = {}
              for key, value in merged.items():
                  if value in (None, [], {}):
                      continue
                  cleaned[key] = value
              return cleaned or None


          # =============================================================================
          # BO / state / bridge helpers
          # =============================================================================

          def _build_bo_meta(bo: dict[str, Any]) -> dict[str, Any]:
              return {
                  "bo_type": _coerce_str(bo.get("BOType")),
                  "asset_cluster_id": _coerce_str(bo.get("AssetClusterId")),
                  "time_text": _coerce_str(bo.get("TimeText")),
                  "time_precision": _coerce_str(bo.get("TimePrecision")),
                  "statement_type": _coerce_str(bo.get("StatementType")),
                  "perspective": _coerce_str(bo.get("Perspective")),
                  "status_tags": _coerce_list(bo.get("StatusTags")),
              }


          def _build_state_context(
              bo: dict[str, Any],
              linked_event_candidates: list[dict[str, Any]],
          ) -> dict[str, Any] | None:
              bo_type = _coerce_str(bo.get("BOType"))
              status_tags = _coerce_list(bo.get("StatusTags"))
              state_candidates = [
                  c for c in linked_event_candidates
                  if _coerce_str(c.get("event_or_state")) == "state"
              ]
              is_state_fact = (bo_type == "state") or bool(state_candidates)
              is_ongoing = bool(set(status_tags).intersection(STATE_STATUS_TAGS)) or any(
                  _coerce_dict(c.get("current_state_candidate")).get("is_ongoing") is True
                  for c in state_candidates
              )
              if not is_state_fact and not status_tags:
                  return None

              state_summaries = _uniq([
                  _coerce_str(_coerce_dict(c.get("current_state_candidate")).get("state_summary"))
                  or _coerce_str(c.get("action_summary"))
                  for c in state_candidates
              ])
              state_as_ofs = _uniq([
                  _coerce_str(_coerce_dict(c.get("current_state_candidate")).get("state_as_of"))
                  for c in state_candidates
              ])

              return {
                  "is_state_fact": is_state_fact,
                  "is_ongoing": is_ongoing,
                  "status_tags": status_tags,
                  "state_summaries": state_summaries,
                  "state_as_ofs": state_as_ofs,
              }


          def _build_standing_context(
              fact: dict[str, Any],
              bo: dict[str, Any],
              evidence_semantics: dict[str, Any],
              linked_event_candidates: list[dict[str, Any]],
              state_context: dict[str, Any] | None,
          ) -> dict[str, Any] | None:
              current_right_holder_candidates: list[str] = []
              current_possessor_candidates: list[str] = []
              current_user_candidates: list[str] = []
              current_benefit_holder_candidates: list[str] = []
              standing_watchpoints: list[str] = []

              def _add(bucket: list[str], values: list[Any]) -> None:
                  for value in values:
                      text = _coerce_str(value)
                      if text:
                          bucket.append(text)

              def _state_name_list(current_state_candidate: dict[str, Any], primary_key: str, legacy_key: str) -> list[str]:
                  return _uniq(
                      _coerce_list(current_state_candidate.get(primary_key))
                      + _coerce_list(current_state_candidate.get(legacy_key))
                  )

              def _candidate_date_key(candidate: dict[str, Any]) -> str:
                  return (
                      _coerce_str(candidate.get("event_date"))
                      or _coerce_str(_coerce_dict(candidate.get("current_state_candidate")).get("state_as_of"))
                      or ""
                  )

              text_pool = [
                  _coerce_str(fact.get("action")) or "",
                  _coerce_str(bo.get("Action")) or "",
                  _coerce_str(fact.get("object_spec")) or "",
                  _coerce_str(bo.get("Object")) or "",
              ]
              status_tags = set(
                  _coerce_list(_coerce_dict(state_context).get("status_tags"))
                  + _coerce_list(bo.get("StatusTags"))
              )

              performer = _coerce_str(bo.get("Performer"))
              if performer and "current_possession" in status_tags:
                  current_possessor_candidates.append(performer)
              if performer and ({"current_use", "current_control"} & status_tags):
                  current_user_candidates.append(performer)
                  current_benefit_holder_candidates.append(performer)

              contract_components_by_index: dict[str, list[dict[str, Any]]] = {}
              for comp in _coerce_obj_list(evidence_semantics.get("contract_components")):
                  evidence_index = _coerce_str(comp.get("evidence_index"))
                  if evidence_index:
                      contract_components_by_index.setdefault(evidence_index, []).append(comp)

              transfer_candidates: list[dict[str, Any]] = []

              for candidate in _coerce_obj_list(linked_event_candidates):
                  participants = _coerce_dict(candidate.get("participants"))
                  actor_candidates = _coerce_list(participants.get("actor_candidates"))
                  counterparty_candidates = _coerce_list(participants.get("counterparty_candidates"))
                  beneficiary_candidates = _coerce_list(participants.get("beneficiary_candidates"))
                  holder_candidates = beneficiary_candidates or counterparty_candidates or actor_candidates

                  event_kind = _coerce_str(candidate.get("event_kind")) or ""
                  action_summary = _coerce_str(candidate.get("action_summary")) or ""
                  current_state_candidate = _coerce_dict(candidate.get("current_state_candidate"))

                  state_possessors = _state_name_list(current_state_candidate, "current_possessor_candidates", "possession_holder_candidates")
                  state_users = _state_name_list(current_state_candidate, "current_user_candidates", "user_holder_candidates")
                  state_benefit_holders = _state_name_list(current_state_candidate, "current_benefit_holder_candidates", "benefit_holder_candidates")
                  state_right_holders = _state_name_list(current_state_candidate, "current_right_holder_candidates", "right_holder_candidates")

                  _add(current_possessor_candidates, state_possessors)
                  _add(current_user_candidates, state_users)
                  _add(current_benefit_holder_candidates, state_benefit_holders)
                  _add(current_right_holder_candidates, state_right_holders)

                  if _coerce_str(candidate.get("event_or_state")) == "state":
                      if not state_possessors and (
                          "점유" in event_kind
                          or "점유" in action_summary
                          or "current_possession" in status_tags
                      ):
                          _add(current_possessor_candidates, actor_candidates or beneficiary_candidates)

                      if (
                          "사용" in action_summary
                          or "영업" in action_summary
                          or "운영" in action_summary
                          or "관리" in action_summary
                          or "current_use" in status_tags
                          or "current_control" in status_tags
                      ):
                          _add(current_user_candidates, actor_candidates or beneficiary_candidates)
                          _add(current_benefit_holder_candidates, actor_candidates or beneficiary_candidates)

                  if event_kind in {"소유권이전", "증여", "경매매각", "처분"}:
                      transfer_candidates.append(candidate)

                  if event_kind == "임대차":
                      _add(current_user_candidates, holder_candidates)

                  if event_kind == "인도" or "인도" in action_summary:
                      _add(current_possessor_candidates, holder_candidates)
                      _add(current_user_candidates, holder_candidates)

                  for comp in contract_components_by_index.get(_coerce_str(candidate.get("evidence_index")) or "", []):
                      kind = _coerce_str(comp.get("component_kind"))
                      if kind in {"lease", "management_use_shift", "business_transfer"}:
                          _add(current_user_candidates, holder_candidates)
                          _add(current_benefit_holder_candidates, holder_candidates)
                      elif kind == "delivery":
                          _add(current_possessor_candidates, holder_candidates)
                          _add(current_user_candidates, holder_candidates)
                      elif kind in {"sale", "building_sale"}:
                          _add(current_user_candidates, holder_candidates)

              if transfer_candidates:
                  latest_transfer = sorted(transfer_candidates, key=_candidate_date_key)[-1]
                  participants = _coerce_dict(latest_transfer.get("participants"))
                  latest_holders = (
                      _coerce_list(participants.get("beneficiary_candidates"))
                      or _coerce_list(participants.get("counterparty_candidates"))
                      or _coerce_list(participants.get("actor_candidates"))
                  )
                  _add(current_right_holder_candidates, latest_holders)

              if _coerce_obj_list(evidence_semantics.get("valuation_contexts")):
                  _add(current_benefit_holder_candidates, current_user_candidates or current_possessor_candidates)
                  if _coerce_obj_list(evidence_semantics.get("contract_components")):
                      standing_watchpoints.append("USE_GAIN_AND_CONTRACT_SPLIT_REVIEW")

              succession_or_transfer_sensitive = any(
                  keyword in text
                  for text in text_pool
                  for keyword in ["상속", "승계", "증여", "경매", "매각", "소유권이전"]
                  if text
              )
              if succession_or_transfer_sensitive and len(_uniq(current_right_holder_candidates)) != 1:
                  standing_watchpoints.append("CURRENT_RIGHT_HOLDER_REVIEW")

              has_current_possession_signal = (
                  "current_possession" in status_tags
                  or any(
                      _coerce_str(candidate.get("event_or_state")) == "state"
                      and (
                          "점유" in (_coerce_str(candidate.get("event_kind")) or "")
                          or "점유" in (_coerce_str(candidate.get("action_summary")) or "")
                          or bool(_coerce_list(_coerce_dict(candidate.get("current_state_candidate")).get("current_possessor_candidates")) or _coerce_list(_coerce_dict(candidate.get("current_state_candidate")).get("possession_holder_candidates")))
                      )
                      for candidate in _coerce_obj_list(linked_event_candidates)
                  )
              )
              if has_current_possession_signal and not _uniq(current_possessor_candidates):
                  standing_watchpoints.append("CURRENT_POSSESSOR_REVIEW")

              result = {
                  "current_right_holder_candidates": _uniq(current_right_holder_candidates),
                  "current_possessor_candidates": _uniq(current_possessor_candidates),
                  "current_user_candidates": _uniq(current_user_candidates),
                  "current_benefit_holder_candidates": _uniq(current_benefit_holder_candidates),
                  "standing_watchpoints": _uniq(standing_watchpoints),
              }

              if not any(result.values()):
                  return None
              return result


          def _bridge_legacy_fields(
              fact: dict[str, Any],
              bo: dict[str, Any],
              merged_lco: dict[str, Any],
              evidence_semantics: dict[str, Any],
              linked_event_candidates: list[dict[str, Any]],
              state_context: dict[str, Any] | None,
          ) -> dict[str, Any]:
              bridged_keywords: list[str] = []

              bridged_keywords.extend(_coerce_list(bo.get("Legal_Keywords")))
              bridged_keywords.extend(_coerce_list(merged_lco.get("claim_status_tags")))

              for comp in _coerce_obj_list(evidence_semantics.get("contract_components")):
                  kind = _coerce_str(comp.get("component_kind"))
                  obj = _coerce_str(comp.get("object_spec"))
                  if kind == "lease":
                      bridged_keywords.extend(["임대차"])
                  elif kind in {"sale", "building_sale"}:
                      bridged_keywords.extend(["매매"])
                  elif kind == "delivery":
                      bridged_keywords.extend(["인도"])
                  elif kind == "management_use_shift":
                      bridged_keywords.extend(["관리", "사용"])
                  if obj and "건물" in obj:
                      bridged_keywords.append("건물")
                  if obj and "대지" in obj:
                      bridged_keywords.append("토지")

              for val in _coerce_obj_list(evidence_semantics.get("valuation_contexts")):
                  kind = _coerce_str(val.get("valuation_kind"))
                  if kind == "rent_value":
                      bridged_keywords.extend(["차임시세", "사용수익"])
                  elif kind == "market_value":
                      bridged_keywords.extend(["시가"])

              for candidate in linked_event_candidates:
                  event_kind = _coerce_str(candidate.get("event_kind"))
                  bridged_keywords.extend(EVENT_KIND_TO_KEYWORDS.get(event_kind or "", []))

              if state_context:
                  if state_context.get("is_ongoing"):
                      bridged_keywords.append("현재")
                  bridged_keywords.extend(_coerce_list(state_context.get("status_tags")))

              bridged_claim_status_tags = _normalize_array(
                  _coerce_list(merged_lco.get("claim_status_tags"))
                  + _coerce_list(state_context.get("status_tags") if state_context else [])
              )

              bridged_object_spec = _coerce_str(fact.get("object_spec")) or _coerce_str(bo.get("Object"))
              if not bridged_object_spec:
                  component_objects = [
                      _coerce_str(comp.get("object_spec"))
                      for comp in _coerce_obj_list(evidence_semantics.get("contract_components"))
                      if _coerce_str(comp.get("object_spec"))
                  ]
                  if len(component_objects) == 1:
                      bridged_object_spec = component_objects[0]

              bridged_amount = None
              if not _coerce_str(fact.get("amount")):
                  component_amounts = [
                      _coerce_str(comp.get("amount"))
                      for comp in _coerce_obj_list(evidence_semantics.get("contract_components"))
                      if _coerce_str(comp.get("amount"))
                  ]
                  if len(_uniq(component_amounts)) == 1:
                      bridged_amount = component_amounts[0]

              return {
                  "legal_keywords": _normalize_keyword_list(bridged_keywords),
                  "claim_status_tags": bridged_claim_status_tags,
                  "object_spec": bridged_object_spec,
                  "amount": bridged_amount,
              }


          def _validate_item_semantics(item: dict[str, Any]) -> list[str]:
              warnings: list[str] = []
              fact_id = _coerce_str(item.get("fact_id")) or "UNKNOWN"

              evidence_semantics = _coerce_dict(item.get("evidence_semantics"))
              flags = set(_coerce_list(evidence_semantics.get("doc_semantic_flags")))
              source_trace = _coerce_dict(item.get("source_trace"))
              structured_presence_flags = _coerce_dict(item.get("structured_presence_flags"))

              if "mixed_contract_doc" in flags and not _coerce_list(source_trace.get("linked_evidence_indexes_with_contract_components")):
                  warnings.append(f"VALIDATION WARNING: fact_id={fact_id} mixed contract evidence exists but contract_components were not projected")

              if "valuation_doc" in flags and not _coerce_list(source_trace.get("linked_evidence_indexes_with_valuation_object")):
                  warnings.append(f"VALIDATION WARNING: fact_id={fact_id} valuation evidence exists but valuation_object was not projected")

              if "mixed_contract_doc" in flags and not _coerce_obj_list(evidence_semantics.get("contract_components")):
                  warnings.append(f"VALIDATION WARNING: fact_id={fact_id} mixed_contract_doc semantics dropped")

              if "valuation_doc" in flags and not _coerce_obj_list(evidence_semantics.get("valuation_contexts")):
                  warnings.append(f"VALIDATION WARNING: fact_id={fact_id} valuation_doc semantics dropped")

              if _coerce_list(evidence_semantics.get("multi_event_registry_indexes")) and not _coerce_obj_list(item.get("linked_event_candidates")):
                  warnings.append(f"VALIDATION WARNING: fact_id={fact_id} registry/event candidate projection missing")

              linked_event_candidates = _coerce_obj_list(item.get("linked_event_candidates"))
              has_current_possession_candidate = any(
                  _coerce_str(candidate.get("event_or_state")) == "state"
                  and (
                      "점유" in (_coerce_str(candidate.get("event_kind")) or "")
                      or "점유" in (_coerce_str(candidate.get("action_summary")) or "")
                      or bool(_coerce_list(_coerce_dict(candidate.get("current_state_candidate")).get("current_possessor_candidates")) or _coerce_list(_coerce_dict(candidate.get("current_state_candidate")).get("possession_holder_candidates")))
                  )
                  for candidate in linked_event_candidates
              )
              if has_current_possession_candidate and not bool(structured_presence_flags.get("has_state_context")):
                  warnings.append(f"VALIDATION WARNING: fact_id={fact_id} current possession state candidate exists but state_context is missing")

              bo_meta = _coerce_dict(item.get("bo_meta"))
              state_context = _coerce_dict(item.get("state_context"))
              if _coerce_str(bo_meta.get("bo_type")) == "state" and not state_context:
                  warnings.append(f"VALIDATION WARNING: fact_id={fact_id} state BO missing state_context")

              return _uniq(warnings)


          # =============================================================================
          # Item builder
          # =============================================================================

          def _build_item(
              fact: dict[str, Any],
              bo: dict[str, Any],
              fact_id_by_bo: dict[str, str],
              evidence_indexed_by_id: dict[str, dict[str, Any]],
              event_candidates_by_index: dict[str, dict[str, Any]],
              fact_actio_by_bo: dict[str, dict[str, Any]],
              evidence_actio_by_index: dict[str, dict[str, Any]],
              actio_case_signals: dict[str, Any] | None,
          ) -> dict[str, Any]:
              bo_evidence = bo.get("Evidence") or []

              evidence_refs = _normalize_evidence_refs(fact.get("evidence_refs"), bo_evidence)
              evidence_indexes = _uniq(
                  _parse_evidence_indexes_from_refs(evidence_refs)
                  + [
                      _coerce_str(evidence.get("evidence_index"))
                      for evidence in bo_evidence
                      if _coerce_str(evidence.get("evidence_index"))
                  ]
              )
              evidence_titles = _extract_evidence_titles(evidence_refs, bo_evidence)

              linked_evidence_legal_calcs: list[dict[str, Any]] = []
              linked_evidence_indexes_with_legal_calc: list[str] = []
              for evidence_index in evidence_indexes:
                  record = evidence_indexed_by_id.get(evidence_index)
                  if isinstance(record, dict) and isinstance(record.get("legal_calculation_object"), dict):
                      linked_evidence_legal_calcs.append(record["legal_calculation_object"])
                      linked_evidence_indexes_with_legal_calc.append(evidence_index)

              merged_lco, lco_field_sources, lco_conflicts = _merge_legal_calculation_object(
                  fact.get("legal_calculation_object"),
                  linked_evidence_legal_calcs,
              )

              evidence_semantics = _build_evidence_semantics(
                  evidence_indexes=evidence_indexes,
                  evidence_indexed_by_id=evidence_indexed_by_id,
              )

              linked_event_candidates, linked_evidence_indexes_with_event_candidates = _project_linked_event_candidates(
                  fact=fact,
                  bo=bo,
                  evidence_indexes=evidence_indexes,
                  event_candidates_by_index=event_candidates_by_index,
              )

              bo_meta = _build_bo_meta(bo)
              state_context = _build_state_context(
                  bo=bo,
                  linked_event_candidates=linked_event_candidates,
              )
              standing_context = _build_standing_context(
                  fact=fact,
                  bo=bo,
                  evidence_semantics=evidence_semantics,
                  linked_event_candidates=linked_event_candidates,
                  state_context=state_context,
              )

              claim_event_type = _infer_claim_event_type(merged_lco, bo)
              amount, amount_source_field = _derive_amount_and_source_field(fact.get("amount"), merged_lco)

              bridged = _bridge_legacy_fields(
                  fact=fact,
                  bo=bo,
                  merged_lco=merged_lco,
                  evidence_semantics=evidence_semantics,
                  linked_event_candidates=linked_event_candidates,
                  state_context=state_context,
              )
              if not amount and bridged.get("amount"):
                  amount = bridged["amount"]
                  amount_source_field = "semantic_bridge"

              actio_support = _merge_actio_support(
                  source_bo_id=_coerce_str(fact.get("source_bo_id")),
                  evidence_indexes=evidence_indexes,
                  fact_actio_by_bo=fact_actio_by_bo,
                  evidence_actio_by_index=evidence_actio_by_index,
                  actio_case_signals=actio_case_signals,
              )
              action_label = _extract_action_label(bo)

              all_parties = _uniq(
                  _coerce_list(fact.get("parties"))
                  + [
                      _coerce_str(bo.get("Performer")),
                      _coerce_str(bo.get("Subject")),
                      _coerce_str(merged_lco.get("claim_creditor")),
                      _coerce_str(merged_lco.get("claim_debtor")),
                      _coerce_str(merged_lco.get("claim_guarantor")),
                      _coerce_str(merged_lco.get("claim_security_provider")),
                  ]
              )
              performer = _coerce_str(bo.get("Performer"))
              subject = _coerce_str(bo.get("Subject"))
              other_parties = [
                  party
                  for party in _uniq(_coerce_list(fact.get("parties")))
                  if party not in {performer, subject}
              ]

              item = {
                  # legacy / downstream-compatible core
                  "fact_id": _coerce_str(fact.get("fact_id")),
                  "bo_id": _coerce_str(fact.get("source_bo_id")),
                  "reason_bo_id": _coerce_str(bo.get("Reason")),
                  "prior_bo_id": _coerce_str(bo.get("PriorAct")),
                  "reason_fact_id": _coerce_str(fact_id_by_bo.get(bo.get("Reason"))),
                  "prior_fact_id": _coerce_str(fact_id_by_bo.get(bo.get("PriorAct"))),

                  "event_date": _first_non_empty(fact.get("date"), bo.get("BehaviorTime")),
                  "event_class": _strip_event_class(_first_non_empty(fact.get("type"), bo.get("ActionType"))),

                  # action source discipline
                  "action_label": action_label,
                  "action_text": _first_non_empty(bo.get("Action"), fact.get("action")),
                  "fact_action_text": _coerce_str(fact.get("action")),
                  "bo_action_text": _coerce_str(bo.get("Action")),

                  "juristic_act": {
                      "label": _coerce_str((bo.get("JuristicAct") or {}).get("label") if isinstance(bo.get("JuristicAct"), dict) else None),
                      "gubun_multi": _coerce_list((bo.get("JuristicAct") or {}).get("gubun_multi") if isinstance(bo.get("JuristicAct"), dict) else []),
                      "basis_note": _coerce_str((bo.get("JuristicAct") or {}).get("basis_note") if isinstance(bo.get("JuristicAct"), dict) else None),
                      "needs_review": (bo.get("JuristicAct") or {}).get("needs_review") if isinstance(bo.get("JuristicAct"), dict) else None,
                  },

                  # party directionality preserved
                  "party_roles": {
                      "performer": performer,
                      "performer_type": _coerce_str(bo.get("PerformerType")),
                      "subject": subject,
                      "claim_creditor": _coerce_str(merged_lco.get("claim_creditor")),
                      "claim_debtor": _coerce_str(merged_lco.get("claim_debtor")),
                      "claim_guarantor": _coerce_str(merged_lco.get("claim_guarantor")),
                      "claim_security_provider": _coerce_str(merged_lco.get("claim_security_provider")),
                      "all_parties": all_parties,
                      "other_parties": other_parties,
                  },
                  "actors": _uniq(_coerce_list(fact.get("parties")) + [performer, subject]),

                  # event object / amount
                  "object_spec": _first_non_empty(
                      fact.get("object_spec"),
                      bo.get("Object"),
                      bridged.get("object_spec"),
                  ),
                  "fact_object_spec": _coerce_str(fact.get("object_spec")),
                  "bo_object": _coerce_str(bo.get("Object")),
                  "location": _coerce_str(bo.get("Location")),
                  "method": _coerce_str(bo.get("Method")),
                  "outcome": _coerce_str(bo.get("Outcome")),
                  "amount": amount,
                  "amount_source_field": amount_source_field,

                  # structured property / transaction fields
                  "property_transaction": {
                      "asset_id": _coerce_str(merged_lco.get("asset_id")),
                      "property_label_for_schedule": _coerce_str(merged_lco.get("property_label_for_schedule")),
                      "transaction_type": _coerce_str(merged_lco.get("transaction_type")),
                      "transaction_date": _coerce_str(merged_lco.get("transaction_date")),
                      "registration_date": _coerce_str(merged_lco.get("registration_date")),
                      "registry_office": _coerce_str(merged_lco.get("registry_office")),
                      "registry_receipt_no": _coerce_str(merged_lco.get("registry_receipt_no")),
                      "registry_recorded_transfer_date": _coerce_str(merged_lco.get("registry_recorded_transfer_date")),
                      "market_value_at_act": _coerce_str(merged_lco.get("market_value_at_act")),
                      "market_value_at_close": _coerce_str(merged_lco.get("market_value_at_close")),
                      "sale_price": _coerce_str(merged_lco.get("sale_price")),
                      "consideration_breakdown": _normalize_array(merged_lco.get("consideration_breakdown")),
                  },

                  # structured claim fields
                  "claim_id": _coerce_str(merged_lco.get("claim_id")),
                  "claim_label_for_schedule": _coerce_str(merged_lco.get("claim_label_for_schedule")),
                  "claim_event_type": claim_event_type,
                  "claim_nature_tags": _normalize_array(merged_lco.get("claim_nature_tags")),
                  "claim_creditor": _coerce_str(merged_lco.get("claim_creditor")),
                  "claim_debtor": _coerce_str(merged_lco.get("claim_debtor")),
                  "claim_guarantor": _coerce_str(merged_lco.get("claim_guarantor")),
                  "claim_security_provider": _coerce_str(merged_lco.get("claim_security_provider")),
                  "claim_arising_date": _coerce_str(merged_lco.get("claim_arising_date")),
                  "claim_maturity_date": _coerce_str(merged_lco.get("claim_maturity_date")),
                  "claim_default_or_acceleration_date": _coerce_str(merged_lco.get("claim_default_or_acceleration_date")),
                  "claim_performance_date": _coerce_str(merged_lco.get("claim_performance_date")),
                  "claim_instrument_date": _coerce_str(merged_lco.get("claim_instrument_date")),
                  "claim_instrument_identifier": _coerce_str(merged_lco.get("claim_instrument_identifier")),
                  "claim_instrument_issuer": _coerce_str(merged_lco.get("claim_instrument_issuer")),
                  "claim_principal_amount_at_act": _coerce_str(merged_lco.get("claim_principal_amount_at_act")),
                  "claim_total_amount_at_act": _coerce_str(merged_lco.get("claim_total_amount_at_act")),
                  "claim_outstanding_amount_at_close": _coerce_str(merged_lco.get("claim_outstanding_amount_at_close")),
                  "claim_actual_performance_amount": _coerce_str(merged_lco.get("claim_actual_performance_amount")),
                  "claim_guarantee_limit_amount": _coerce_str(merged_lco.get("claim_guarantee_limit_amount")),
                  "claim_secured_cap_amount": _coerce_str(merged_lco.get("claim_secured_cap_amount")),
                  "claim_interest_rate": _coerce_str(merged_lco.get("claim_interest_rate")),
                  "claim_default_rate": _coerce_str(merged_lco.get("claim_default_rate")),
                  "claim_component_breakdown": _normalize_array(merged_lco.get("claim_component_breakdown")),
                  "claim_status_tags": _normalize_array(
                      _coerce_list(merged_lco.get("claim_status_tags"))
                      + _coerce_list(bridged.get("claim_status_tags"))
                  ),

                  # BO keywords and evidence trace
                  "legal_keywords": _normalize_keyword_list(
                      _coerce_list(bo.get("Legal_Keywords"))
                      + _coerce_list(bridged.get("legal_keywords"))
                  ),
                  "evidence_refs": evidence_refs,
                  "evidence_indexes": evidence_indexes,
                  "evidence_titles": evidence_titles,
                  "credibility": _coerce_str(fact.get("credibility")),
                  "evidence_strength": _build_evidence_strength(bo_evidence, evidence_indexes),

                  # new semantic projections
                  "bo_meta": bo_meta,
                  "state_context": state_context,
                  "standing_context": standing_context,
                  "evidence_semantics": evidence_semantics,
                  "linked_event_candidates": linked_event_candidates,

                  # source transparency
                  "source_trace": {
                      "linked_evidence_indexes_with_legal_calc": linked_evidence_indexes_with_legal_calc,
                      "linked_evidence_indexes_with_event_candidates": linked_evidence_indexes_with_event_candidates,
                      "linked_evidence_indexes_with_contract_components": _coerce_list(evidence_semantics.get("contract_component_source_indexes")),
                      "linked_evidence_indexes_with_valuation_object": _coerce_list(evidence_semantics.get("valuation_source_indexes")),
                      "legal_calculation_field_sources": lco_field_sources,
                      "legal_calculation_conflicts": lco_conflicts,
                      "used_fact_ledger_legal_calc": bool(fact.get("legal_calculation_object")),
                      "used_linked_evidence_backfill": bool(linked_evidence_indexes_with_legal_calc),
                      "used_linked_event_candidate_projection": bool(linked_event_candidates),
                      "used_semantic_keyword_bridge": bool(bridged.get("legal_keywords")),
                  },

                  # fast completeness hints for downstream claim drafting
                  "structured_presence_flags": {
                      "has_claim_role_direction": bool(
                          _coerce_str(merged_lco.get("claim_creditor"))
                          and _coerce_str(merged_lco.get("claim_debtor"))
                      ),
                      "has_claim_amount_terms": bool(
                          _coerce_str(merged_lco.get("claim_principal_amount_at_act"))
                          or _coerce_str(merged_lco.get("claim_total_amount_at_act"))
                          or _coerce_str(merged_lco.get("claim_outstanding_amount_at_close"))
                          or _coerce_str(merged_lco.get("claim_actual_performance_amount"))
                          or _coerce_str(merged_lco.get("claim_guarantee_limit_amount"))
                          or _coerce_str(merged_lco.get("claim_secured_cap_amount"))
                          or amount
                      ),
                      "has_claim_date_terms": bool(
                          _coerce_str(merged_lco.get("claim_arising_date"))
                          or _coerce_str(merged_lco.get("claim_maturity_date"))
                          or _coerce_str(merged_lco.get("claim_default_or_acceleration_date"))
                          or _coerce_str(merged_lco.get("claim_performance_date"))
                      ),
                      "has_rate_terms": bool(
                          _coerce_str(merged_lco.get("claim_interest_rate"))
                          or _coerce_str(merged_lco.get("claim_default_rate"))
                      ),
                      "has_property_transfer_terms": bool(
                          _coerce_str(merged_lco.get("asset_id"))
                          or _coerce_str(merged_lco.get("transaction_type"))
                          or _coerce_str(merged_lco.get("sale_price"))
                          or _coerce_str(merged_lco.get("registration_date"))
                          or _coerce_str(merged_lco.get("registry_recorded_transfer_date"))
                      ),
                      "has_state_context": bool(state_context),
                      "has_standing_context": bool(standing_context),
                      "has_linked_event_candidates": bool(linked_event_candidates),
                      "has_contract_components": bool(_coerce_obj_list(evidence_semantics.get("contract_components"))),
                      "has_valuation_context": bool(_coerce_obj_list(evidence_semantics.get("valuation_contexts"))),
                      "has_cluster_context": False,
                      "has_actio_support": bool(actio_support),
                  },

                  # optional stage1 support artifacts
                  "actio_pauliana_support": actio_support,
              }

              item_warnings = _validate_item_semantics(item)
              if item_warnings:
                  item["validation_warnings"] = item_warnings

              return item


          # =============================================================================
          # View builder
          # =============================================================================

          def _attach_cluster_context(items: list[dict[str, Any]]) -> None:
              by_asset_cluster: dict[str, list[dict[str, Any]]] = {}
              by_property_cluster: dict[str, list[dict[str, Any]]] = {}

              for item in items:
                  bo_meta = _coerce_dict(item.get("bo_meta"))
                  asset_cluster_id = _coerce_str(bo_meta.get("asset_cluster_id"))
                  if asset_cluster_id:
                      by_asset_cluster.setdefault(asset_cluster_id, []).append(item)

                  evidence_semantics = _coerce_dict(item.get("evidence_semantics"))
                  for cluster_id in _coerce_list(evidence_semantics.get("property_cluster_ids")):
                      by_property_cluster.setdefault(cluster_id, []).append(item)

              for item in items:
                  bo_meta = _coerce_dict(item.get("bo_meta"))
                  evidence_semantics = _coerce_dict(item.get("evidence_semantics"))
                  asset_cluster_id = _coerce_str(bo_meta.get("asset_cluster_id"))
                  property_cluster_ids = _coerce_list(evidence_semantics.get("property_cluster_ids"))

                  related_asset_items = by_asset_cluster.get(asset_cluster_id, []) if asset_cluster_id else []
                  related_property_items: list[dict[str, Any]] = []
                  for cluster_id in property_cluster_ids:
                      related_property_items.extend(by_property_cluster.get(cluster_id, []))
                  related_property_items = _uniq(related_property_items)

                  item["cluster_context"] = {
                      "asset_cluster_id": asset_cluster_id,
                      "property_cluster_ids": property_cluster_ids,
                      "related_fact_ids_same_asset_cluster": _uniq([
                          _coerce_str(i.get("fact_id")) for i in related_asset_items
                          if _coerce_str(i.get("fact_id")) and _coerce_str(i.get("fact_id")) != _coerce_str(item.get("fact_id"))
                      ]),
                      "related_bo_ids_same_asset_cluster": _uniq([
                          _coerce_str(i.get("bo_id")) for i in related_asset_items
                          if _coerce_str(i.get("bo_id")) and _coerce_str(i.get("bo_id")) != _coerce_str(item.get("bo_id"))
                      ]),
                      "related_fact_ids_same_property_cluster": _uniq([
                          _coerce_str(i.get("fact_id")) for i in related_property_items
                          if _coerce_str(i.get("fact_id")) and _coerce_str(i.get("fact_id")) != _coerce_str(item.get("fact_id"))
                      ]),
                      "related_evidence_indexes_same_property_cluster": _uniq([
                          idx
                          for i in related_property_items
                          for idx in _coerce_list(i.get("evidence_indexes"))
                          if idx not in _coerce_list(item.get("evidence_indexes"))
                      ]),
                  }

                  item.setdefault("structured_presence_flags", {})
                  item["structured_presence_flags"]["has_cluster_context"] = bool(
                      asset_cluster_id or property_cluster_ids
                  )


          def _build_view_validation_warnings(items: list[dict[str, Any]]) -> list[str]:
              warnings: list[str] = []
              for item in items:
                  warnings.extend(_coerce_list(item.get("validation_warnings")))
              return _uniq(warnings)


          def _build_view(
              facts: list[dict[str, Any]],
              bos: list[dict[str, Any]],
              evidence_indexed: list[dict[str, Any]] | None,
              evidence_event_candidates: dict[str, Any] | None,
              fact_actio_support: list[dict[str, Any]] | None,
              evidence_actio_support: list[dict[str, Any]] | None,
              actio_case_signals: dict[str, Any] | None,
              source_files_used: list[str],
          ) -> dict[str, Any]:
              evidence_indexed = evidence_indexed or []
              fact_actio_support = fact_actio_support or []
              evidence_actio_support = evidence_actio_support or []

              bo_index = {
                  _coerce_str(bo.get("id")): bo
                  for bo in bos
                  if _coerce_str(bo.get("id"))
              }
              fact_id_by_bo = {
                  _coerce_str(fact.get("source_bo_id")): _coerce_str(fact.get("fact_id"))
                  for fact in facts
                  if _coerce_str(fact.get("source_bo_id"))
              }
              evidence_indexed_by_id = {
                  _coerce_str(doc.get("evidence_index")): doc
                  for doc in evidence_indexed
                  if _coerce_str(doc.get("evidence_index"))
              }
              event_candidates_by_index = {
                  _coerce_str(doc.get("evidence_index")): doc
                  for doc in _coerce_obj_list((evidence_event_candidates or {}).get("items"))
                  if _coerce_str(doc.get("evidence_index"))
              }
              fact_actio_by_bo = {
                  _coerce_str(item.get("source_bo_id")): item
                  for item in fact_actio_support
                  if _coerce_str(item.get("source_bo_id"))
              }
              evidence_actio_by_index = {
                  _coerce_str(item.get("evidence_index")): item
                  for item in evidence_actio_support
                  if _coerce_str(item.get("evidence_index"))
              }

              items: list[dict[str, Any]] = []
              for fact in facts:
                  bo = bo_index.get(_coerce_str(fact.get("source_bo_id"))) or {}
                  items.append(
                      _build_item(
                          fact=fact,
                          bo=bo,
                          fact_id_by_bo=fact_id_by_bo,
                          evidence_indexed_by_id=evidence_indexed_by_id,
                          event_candidates_by_index=event_candidates_by_index,
                          fact_actio_by_bo=fact_actio_by_bo,
                          evidence_actio_by_index=evidence_actio_by_index,
                          actio_case_signals=actio_case_signals,
                      )
                  )

              _attach_cluster_context(items)
              validation_warnings = _build_view_validation_warnings(items)

              case_context: dict[str, Any] | None = None
              if isinstance(actio_case_signals, dict):
                  candidate = {
                      "is_actio_pauliana_suspected": actio_case_signals.get("is_actio_pauliana_suspected"),
                      "suspicion_level": actio_case_signals.get("suspicion_level"),
                      "suspicion_reasons": actio_case_signals.get("suspicion_reasons") or [],
                      "related_bo_ids": actio_case_signals.get("related_bo_ids") or [],
                      "related_evidence_indexes": actio_case_signals.get("related_evidence_indexes") or [],
                  }
                  if any(value not in (None, [], {}) for value in candidate.values()):
                      case_context = candidate

              return {
                  "schema_version": SCHEMA_VERSION,
                  "source_files_used": source_files_used,
                  "case_context": case_context,
                  "validation_warnings": validation_warnings,
                  "items": items,
              }


          # =============================================================================
          # Main
          # =============================================================================

          def main() -> None:
              _init_mcp_session()

              facts = _read_required_json_doc("Fact_Ledger_base.json", msg_id=10)
              bos = _read_required_json_doc("BO.json", msg_id=11)

              if not isinstance(facts, list):
                  raise RuntimeError("Fact_Ledger_base.json must be a JSON array")
              if not isinstance(bos, list):
                  raise RuntimeError("BO.json must be a JSON array")

              source_files_used = ["Fact_Ledger_base.json", "BO.json"]
              validation_warnings: list[str] = []

              evidence_indexed_raw = _read_optional_json_doc("evidence_indexed.json", msg_id=12)
              evidence_indexed = _unwrap_evidence_indexed_payload(evidence_indexed_raw)
              if evidence_indexed is not None:
                  source_files_used.append("evidence_indexed.json")
              else:
                  if evidence_indexed_raw not in (None, {}, []):
                      validation_warnings.append(
                          "VALIDATION WARNING: evidence_indexed.json top-level shape mismatch; semantic evidence projection skipped."
                      )
                  evidence_indexed = None

              evidence_event_candidates_raw = _read_optional_json_doc("evidence_event_candidates.json", msg_id=13)
              evidence_event_candidates = _unwrap_event_candidates_payload(evidence_event_candidates_raw)
              if evidence_event_candidates is not None:
                  source_files_used.append("evidence_event_candidates.json")
              else:
                  if evidence_event_candidates_raw not in (None, {}, []):
                      validation_warnings.append(
                          "VALIDATION WARNING: evidence_event_candidates.json top-level shape mismatch; event/state projection skipped."
                      )
                  evidence_event_candidates = None

              fact_actio_support = _read_optional_json_doc("fact_actio_support.json", msg_id=14)
              if isinstance(fact_actio_support, list):
                  source_files_used.append("fact_actio_support.json")
              else:
                  fact_actio_support = None

              evidence_actio_support = _read_optional_json_doc("evidence_actio_support.json", msg_id=15)
              if isinstance(evidence_actio_support, list):
                  source_files_used.append("evidence_actio_support.json")
              else:
                  evidence_actio_support = None

              actio_case_signals = _read_optional_json_doc("actio_case_signals.json", msg_id=16)
              if isinstance(actio_case_signals, dict):
                  source_files_used.append("actio_case_signals.json")
              else:
                  actio_case_signals = None

              view = _build_view(
                  facts=facts,
                  bos=bos,
                  evidence_indexed=evidence_indexed,
                  evidence_event_candidates=evidence_event_candidates,
                  fact_actio_support=fact_actio_support,
                  evidence_actio_support=evidence_actio_support,
                  actio_case_signals=actio_case_signals,
                  source_files_used=source_files_used,
              )
              view["validation_warnings"] = _uniq(
                  validation_warnings + _coerce_list(view.get("validation_warnings"))
              )

              rendered = json.dumps(view, ensure_ascii=False, indent=2)
              _write_doc("claim_identification_view.json", rendered, msg_id=20)
              print(
                  f"[Task_A] claim_identification_view.json written ({len(view['items'])} items)",
                  file=sys.stderr,
              )


          if __name__ == "__main__":
              main()

    - task_name: Task_B
      llm_provider: google
      llm_model: gemini-3.1-pro-preview
      llm_reasoning: high
      llm_verbosity: low
      llm_endpoint: responses
      preflight: true
      use_tools:
      - localdocs
      prompts:
      - role: user
        content: |
          <COMMON_CACHE_PREFIX_V1_SLIM>
          당신은 대한민국 민사소송 실무를 지원하는 정밀 법률 파이프라인 LLM이다.
          당신의 모든 응답은 후속 자동 단계의 직접 입력이 되므로, 구조적 안정성, 근거 통제, 재현 가능성, 보수적 판단이 우선한다.

          <ABSOLUTE_SYSTEM_RULES>
          - You MUST execute every single instruction provided below without any omission.
          - DO NOT arbitrarily reduce, summarize, or skip any steps, reasoning processes, or outputs.
          - DO NOT use placeholders like "..." or "rest of the code/text". Provide the exhaustive and complete output.
          - Adhere strictly to these rules before processing the prompt.
          </ABSOLUTE_SYSTEM_RULES>

          <base_rules>
          - 입력 파일에 없는 사건 고유 사실, 당사자, 청구, 금액, 날짜, 문서명, 증거, 법률효과를 창안하지 말라.
          - 일반 법지식은 해석 보조로만 사용할 수 있다. 사건별 사실 존재 여부는 반드시 입력 파일에서만 찾는다.
          - 모호하면 추정으로 메우지 말고 completeness, review_flags, excluded_items를 사용하라.
          - 현재 단계에서 지정된 파일만 읽고, 지정된 순서와 필드 범위를 우선한다.
          - stage-specific block이 더 구체적인 입력 범위, 판단 기준, 출력 스키마, 정렬 규칙을 제시하면 그 규칙을 우선한다.
          </base_rules>

          <identifier_and_output_rules>
          - 최종 응답은 반드시 JSON 객체 하나만 반환한다.
          - JSON 외의 설명문, 서문, 해설, 마크다운, 코드펜스, 주석을 절대 출력하지 말라.
          - 허용되지 않은 새 키를 만들지 말라.
          - stage-specific block이 정한 키 이름, 키 순서, 배열 이름, 파일 이름을 그대로 지켜라.
          - upstream 식별자와 앵커는 복사할 때 원형을 유지한다. 특히 fact_id, source_fact_ids, bo_id, evidence_index는 바꾸지 말라.
          - 다만 최종 출력 claim_id는 stage-specific output contract에 따라 최종 정렬 후 새로 부여할 수 있다.
          - 동일 입력이면 가능한 한 동일 출력이 나오도록 안정적으로 판단하라.
          </identifier_and_output_rules>

          <focus_rules>
          - 현재 단계에서 필요한 새 판단과 최소 출력에만 집중하라.
          - 이전 단계 산출물을 장문으로 재서술하지 말라.
          - 후속 자동처리를 해치는 장식적 문장, 스키마 해설, 메타 코멘트를 금지한다.
          </focus_rules>
          </COMMON_CACHE_PREFIX_V1_SLIM>

          <TASK_B_STATIC_BLOCK_V1_SLIM>
          <role>
          당신은 대한민국 민사소송 원고대리 실무를 지원하는 LLM이다.
          임무는 입력 자료만 근거로 원고가 실제로 소장에서 주장 대상으로 삼을 수 있는 본안 청구권만 식별하고, claims_identified.json을 안정적으로 생성하는 것이다.
          </role>

          <task_design>
          - conditional_claims는 사용하지 않는다.
          - identified_claims 안에서 completeness로 확정 청구와 검토 필요 후보를 구별한다.
          - completeness=null 이면 4문턱이 모두 명확히 충족된 청구권이다.
          - completeness에 태그 배열이 있으면, 본안 청구권의 주장 후보로는 식별되지만 추가 검토가 필요하다는 뜻이다.
          - HARD_EXCLUDED_DEFENDANT는 completeness로 복귀시키지 않는다.
          - 최종 출력 claim_id는 최종 정렬이 끝난 뒤 `C-001`, `C-002`, ... 형식으로 새로 부여한다.
          - identified_claims는 downstream 호환상 plaintiffs/defendants를 배열로 유지하되, 각 항목이 1원고 x 1피고 구조가 되도록 분해한다.
          </task_design>

          <runtime_mission>
          - 원고가 실제로 주장할 수 있는 본안 청구권만 식별하라.
          - 쟁점, 항변, 입증문제, 보전처분, 배경사실은 청구권과 구별하라.
          - 원채권 청구, 보증채무금 청구, 구상금 청구, 사해행위취소 청구, 등기말소 청구, 인도·철거 청구, 부당이득반환 청구, 확인청구는 주문 단위가 다르면 자동 병합하지 말라.
          </runtime_mission>

          <source_order>
          - 읽기 순서와 파일 우선순위는 client_meeting.md -> client_goal.json -> claim_identification_view.json 이다.
          - client_meeting.md는 현재 권리귀속, 현재 점유·사용·운영, 현재 분쟁상대방, 목적물별 처분 chronology, 제3자를 소송상 직접 상대방으로 고려해야 하는 이유를 파악하는 1차 자료다.
          - client_goal.json은 사건군 요약, 원고·피고 universe, constraints, key_facts, issue_clusters, 소송 목표를 보조 확인하는 2차 자료다.
          - client_goal.json의 `issue_clusters`, `plaintiff_capacity_matrix`, `standing_watchpoints`, `current_control_map`가 있으면 이를 standing/현재상태 2차 anchor로 사용한다.
          - claim_identification_view.json은 fact_id anchor, 구조화된 날짜·금액·역할, semantic projection, 증거 강도, actio support를 제공하는 최종 구조화 앵커다.
          - claim_identification_view의 `state_context`, `standing_context`, `linked_event_candidates.current_state_candidate`를 현재 권리귀속·현재 점유·현재 수익귀속의 직접 anchor로 사용한다.
          - 특히 `standing_context.current_right_holder_candidates`, `current_possessor_candidates`, `current_user_candidates`, `current_benefit_holder_candidates`, `standing_watchpoints`가 있으면 actors·통지 발신자·공동상담 구조보다 우선한다.
          - `linked_event_candidates.current_state_candidate` 안의 current_possessor/current_user/current_benefit_holder/current_right_holder candidate 또는 legacy possession/user/benefit/right holder candidate가 있으면 현재 상태 anchor로 즉시 사용한다.
          - 공동 통지, 공동 항의, 공동 소멸청구, 공동 상담은 직접 standing anchor가 아니라 보조 anchor다.
          - source_fact_ids, fact_id anchor, structured date/amount, evidence anchor는 claim_identification_view.json에서만 가져온다.
          - claim_identification_view.json 안에서는 구조화된 claim/party/date/amount 필드를 action_text나 actors보다 우선한다.
          </source_order>

          <reading_scope>
          - client_meeting.md: 문서 전체에서 아래의 명시적 진술만 사용할 수 있다.
          1. 현재 소유자, 현재 등기명의인, 현재 근저당권자, 현재 점유자, 현재 사용자, 현재 운영자, 현재 수익 귀속자
          2. 목적물별 처분 chronology, 후행 담보권자, 부담인수, 실질 수익 귀속, 친족·특수관계
          3. 특정 제3자를 소송 대상으로 고려해야 하는 이유
          4. 현재 누구를 상대로 어떤 구제를 원한다는 직접 서술
          5. 상대방 주장 중 현재 분쟁구조를 드러내는 명시적 진술
          - client_goal.json: 다음 필드만 사용한다.
          primary_goal,
          constraints,
          summary_key_incidents,
          key_facts,
          claim_type_candidates,
          parties.plaintiffs,
          parties.defendants.name,
          parties.defendants.type,
          parties.defendants.asset_status,
          parties.third_parties,
          aliases,
          issue_clusters,
          plaintiff_capacity_matrix,
          standing_watchpoints,
          current_control_map
          - claim_identification_view.json: case_context, validation_warnings, items 배열만 읽고 다음 필드를 사용할 수 있다.
          fact_id, bo_id, reason_bo_id, prior_bo_id, reason_fact_id, prior_fact_id,
          event_date, event_class,
          action_label, action_text, fact_action_text, bo_action_text,
          juristic_act,
          party_roles,
          actors,
          object_spec, fact_object_spec, bo_object,
          location, method, outcome,
          amount, amount_source_field,
          property_transaction,
          claim_id, claim_label_for_schedule, claim_event_type,
          claim_nature_tags,
          claim_creditor, claim_debtor, claim_guarantor, claim_security_provider,
          claim_arising_date, claim_maturity_date, claim_default_or_acceleration_date, claim_performance_date,
          claim_instrument_date, claim_instrument_identifier, claim_instrument_issuer,
          claim_principal_amount_at_act, claim_total_amount_at_act, claim_outstanding_amount_at_close,
          claim_actual_performance_amount, claim_guarantee_limit_amount, claim_secured_cap_amount,
          claim_interest_rate, claim_default_rate,
          claim_component_breakdown, claim_status_tags,
          legal_keywords,
          evidence_refs, evidence_indexes, evidence_titles,
          credibility, evidence_strength,
          bo_meta,
          state_context,
          standing_context,
          evidence_semantics,
          linked_event_candidates,
          cluster_context,
          source_trace,
          structured_presence_flags,
          actio_pauliana_support
          - `standing_context`가 있으면 내부의 `current_right_holder_candidates`, `current_possessor_candidates`, `current_user_candidates`, `current_benefit_holder_candidates`, `standing_watchpoints`를 직접 사용한다.
          - `linked_event_candidates`를 읽을 때는 `current_state_candidate.state_summary`, `state_as_of`, `is_ongoing`, `current_possessor_candidates`, `current_user_candidates`, `current_benefit_holder_candidates`, `current_right_holder_candidates`, `possession_holder_candidates`, `user_holder_candidates`, `benefit_holder_candidates`, `right_holder_candidates`가 있으면 우선 사용한다.
          - `evidence_semantics.contract_components`와 `valuation_contexts`는 present-state relief와 unjust enrichment time-slice 분리에 직접 사용한다.
          </reading_scope>

          <human_lawyer_identification_standard>
          - 청구권 식별의 1차 기준은 `누가 과거에 무엇을 했는가`가 아니라 `현재 누가 어떤 권리 또는 법률상 이익을 보유하고 있고, 현재 누가 그와 충돌하는 지위·점유·수익·담보를 보유하는가`이다.
          - 따라서 각 asset/property/issue cluster마다 아래 순서로 본다.
          1. 현재 권리귀속 지도(current right map) 복원
          2. 현재 침해상태 또는 현재 의무상대방 지도(current infringement / obligation map) 복원
          3. 현재 수익·사용이익·담보이익 귀속 지도(present benefit map) 복원
          4. 그 후에만 본안 청구권 title을 특정한다.
          - 과거 행위 chain은 현재 권리귀속 또는 현재 침해상태를 설명하는 원인사실로 사용한다.
          - 같은 목적물 cluster 안에서도 말소등기, 부담제거, 인도/철거, 부당이득반환, 확인청구는 서로 다른 현재 구제단위가 될 수 있으므로 자동 병합하지 않는다.
          - 현재 점유자, 현재 사용자, 현재 등기명의인, 현재 근저당권자는 과거 행위자보다 우선적으로 검토할 수 있다.
          </human_lawyer_identification_standard>

          <standing_anchor_priority_rules>
          - standing anchor 우선순위는 다음과 같다.
          1. client_goal.plaintiff_capacity_matrix의 cluster별 직접 권리주체 anchor
          2. claim_identification_view.standing_context의 current_right_holder/current_user/current_benefit_holder anchor
          3. direct contract party, direct creditor, direct tenant, direct owner 등 구조화된 권리주체 anchor
          4. 상속·승계 문서에 의해 직접 이어지는 anchor
          5. 가족관계, 공동 내방, 공동 항의, 공동 통지, 공동 소멸청구, 공동 상담, issue_clusters의 복수 candidate_plaintiffs 기재
          - 5번 범주의 공동행동 fact는 단독으로 원고적격을 확정하거나 공동원고를 확장하는 근거가 될 수 없다.
          - 더 좁은 standing anchor가 1명으로 수렴하면 원고는 그 1명으로 좁힌다.
          - tiers 1~4의 standing anchor가 1명으로 수렴하지 않으면, tier 5의 공동행동 fact를 사용해 1명을 임의 선택하지 말라.
          - tiers 1~4가 단일 원고로 수렴하지 않는 current-right / delivery / demolition / unjust-enrichment / confirmation chain은 identified_claims에 sole plaintiff로 성급히 확정하지 말고, `PLAINTIFF_STANDING_REVIEW`를 부여하거나 identified_entry_gate에 따라 excluded_items로 처리하라.
          - 공동 통지 fact는 notice/enforcement fact이지 plaintiff standing fact가 아니다.
          </standing_anchor_priority_rules>

          <defendant_viability_gate>
          - client_goal.json의 constraints 또는 parties.defendants.asset_status에서 아래 표현이 명시되면 해당 피고는 HARD_EXCLUDED_DEFENDANT로 본다.
          - 완전 폐업
          - 집행 가능 재산 없음
          - 집행할 만한 재산 없음
          - 소제기 실효성 없음
          - 회수 실익 없음
          - HARD_EXCLUDED_DEFENDANT는 identified_claims에 절대 넣지 않는다.
          - HARD_EXCLUDED_DEFENDANT는 completeness, review_flags, 우선순위 판단으로 복귀시키지 않는다.
          - HARD_EXCLUDED_DEFENDANT가 포함된 chain은 피고별로 분리하여 다시 본다.
          - 다만 client_goal.json의 defendants에 없다는 이유만으로 제3자를 HARD_EXCLUDED_DEFENDANT로 보지 말라.
          </defendant_viability_gate>

          <chain_and_cluster_rules>
          - claim chain은 사건행위의 시간순 서사만으로 만들지 말고, 먼저 같은 asset cluster / property cluster / issue cluster 안의 현재 권리귀속·현재 침해상태·현재 이익귀속을 복원한 뒤 그 결과를 기준으로 분리한다.
          - cluster 복원에는 client_meeting.md, client_goal.json.issue_clusters, claim_identification_view.json.cluster_context, bo_meta.asset_cluster_id, evidence_semantics.property_cluster_ids를 함께 사용한다.
          - 복수 원고와 복수 피고가 존재하는 경우, 절대 하나의 청구로 평면화하지 말고 [원고] x [피고]의 각 조합을 독립 chain 후보로 검토하라.
          - 같은 chain으로 묶을 때의 우선 앵커는 다음 순서로 본다.
          1. 같은 structured claim anchor: claim_id 또는 claim_label_for_schedule
          2. 같은 claim_event_type + 같은 핵심 역할 방향: claim_creditor / claim_debtor / claim_guarantor / claim_security_provider
          3. 같은 asset_id 또는 같은 property/object cluster + 같은 현재 권리귀속 구조
          4. reason_bo_id 또는 reason_fact_id의 직접 연결
          5. prior_bo_id 또는 prior_fact_id의 보조 연결
          - 같은 목적물 cluster 안에서 아래는 원칙적으로 서로 다른 chain 후보다.
          1. 현재 등기귀속 또는 권리회복을 구하는 chain
          2. 현재 후행 담보부담 제거를 구하는 chain
          3. 현재 점유·사용 상태의 제거를 구하는 chain
          4. 현재 사용수익 또는 이익 반환을 구하는 chain
          5. 임차권 또는 기타 권리관계의 현재 존재 확인을 구하는 chain
          - prior_bo_id / prior_fact_id는 보조 앵커일 뿐이다. state_context, linked_event_candidates의 state, client_meeting의 현재 상태 문장이 더 직접적이면 그것을 우선한다.
          - actio suspected case에서는 같은 목적물 cluster마다 최소한 아래 피고 역할을 각각 스캔한다.
          1. 수익자·양수인·등록명의 취득자
          2. 같은 목적물에 후속 담보권·근저당권·기타 부담을 취득한 자
          3. 처분대가 구조에서 인수채무·부담해소·변제수령으로 실질 이익을 취득한 자
          4. 현재 점유자 또는 현재 수익귀속자
          5. 전득자·후속 취득자
          - 같은 목적물 cluster에서 현재 등기명의인, 현재 근저당권자, 현재 점유자, 현재 사용자가 모두 다르면 말소등기 chain, 부담제거 chain, 인도/철거 chain, 부당이득 chain을 적어도 분리 검토하라.
          </chain_and_cluster_rules>

          <party_direction_rules>
          - plaintiffs와 defendants는 구조화된 역할 필드 우선으로 특정한다.
          - 원고는 기본적으로 `현재 권리귀속` 기준으로 특정한다.
          - 단순히 통지 발송, 항의 동행, 상담 공동의뢰, 사실관계 공동서술만으로 공동원고로 확장하지 말라.
          - 하나의 청구가 한 원고만으로 안정적으로 anchor되면 다른 의뢰인을 기계적으로 함께 넣지 말고 가장 좁은 원고 집합을 택한다.
          - 상속, 승계, 귀속이 완전히 명확하지 않으면 자동으로 넓은 공동원고 구조를 만들지 말고 completeness 또는 excluded_items로 관리하라.
          - `standing_context` 또는 `current_control_map`이 단일 current right holder / current user / current benefit holder를 제시하면, 공동 통지 발신자 전원보다 그 단일 anchor를 우선한다.
          - 같은 cluster에서 공동 통지·공동 최고·공동 행사 fact와 현재 점유·현재 사용·현재 수익귀속 fact가 함께 있더라도, 공동행동 fact 때문에 원고를 공동원고로 넓히지 말라.
          - plaintiffs 판단 우선순위:
          1. client_meeting.md 또는 client_goal.json.issue_clusters에서 특정된 현재 권리주체
          2. claim_creditor
          3. actio claim이면 preserved claim creditor 또는 plaintiff_claim_snapshots에 나타난 채권자
          4. 구상금 청구이면 claim_creditor가 있으면 그것을 우선하고, 없을 때만 claim_performance_date/claim_actual_performance_amount와 performer/subject를 함께 보아 실제 변제자 여부를 판단한다.
          5. 그 외에는 party_roles.performer / party_roles.subject / actors를 보조로만 사용한다.
          - defendants 판단 우선순위:
          1. 물품대금·대여금·대출금 계열은 claim_debtor 또는 현재 채무인수자 여부가 직접 드러난 자
          2. 보증채무금 계열은 claim_guarantor
          3. 말소등기/권리회복 계열은 현재 등기명의인, 현재 근저당권자, 현재 후행 부담권자
          4. 인도/철거 계열은 현재 점유자, 현재 사용자, 현재 관리귀속자
          5. 부당이득반환 계열은 현재 사용수익 귀속자, 현재 점유를 통한 이익 귀속자
          6. 확인청구 계열은 현재 그 권리관계를 다투는 자 또는 그 존부를 부인하는 자
          7. 사해행위취소 계열은 수익자, 현 수익 귀속자, 후행 담보권자·근저당권자, 동일 처분 scheme에서 목적물에 새 부담을 취득한 자, 실질 수익을 취득한 자를 모두 검토한다.
          - actors에 특정인이 없더라도 bo_meta, state_context, linked_event_candidates, evidence_semantics.contract_components, actio_pauliana_support, client_meeting.md가 그 사람을 현재 권리자·부담권자·점유자·수익귀속자로 직접 가리키면 피고 후보에서 누락하지 말라.
          - 같은 chain 안에 복수 피고를 넣으려면 동일한 claim_title과 동일한 주문 구조가 모든 피고에게 그대로 적용되어야 한다.
          - 현재 점유자와 과거 건축자 또는 과거 처분자가 다를 때, 인도·철거·부당이득 계열 피고는 과거 행위자가 아니라 현재 점유자·현재 사용자·현재 수익귀속자를 우선한다.
          - 역할 방향이 구조화 필드와 문장 표현 사이에서 충돌하면 구조화 필드를 우선하되, 충돌 사실은 review_flags에 반영하라.
          </party_direction_rules>

          <remedy_specific_defendant_priority_rules>
          - 구제수단별 피고 선택 우선순위는 아래와 같다.
          1. `소유권이전등기말소 청구`, `근저당권설정등기말소 청구`, 기타 말소등기·부담제거 계열 -> 현재 등기명의인, 현재 근저당권자, 현재 후행 부담권자 우선
          2. `건물인도 청구`, `토지인도 청구`, `건물철거 및 토지인도 청구` -> 현재 점유자, 현재 사용자, 현재 관리귀속자 우선
          3. `부당이득반환 청구` -> 현재 사용수익 귀속자, 현재 점유를 통한 이익 귀속자 우선. 다만 과거 대가수령 또는 실질 이익취득 반환 slice는 별도 chain으로 검토
          4. `임차권확인 청구` 및 기타 확인계열 -> 현재 그 권리관계를 부인하거나 다투는 자 우선
          5. `사해행위취소 청구` -> 수익자, 현 수익 귀속자, 후행 담보권자·근저당권자, 동일 처분 scheme에서 목적물에 새 부담을 취득한 자, 실질 수익을 취득한 자 우선
          - 어떤 구제수단이든 current-state anchor가 존재하면 과거 행위자, 과거 계약체결자, 과거 신축자, 과거 임대인·매도인보다 current-state holder를 먼저 검토한다.
          - 따라서 remedy를 먼저 특정한 뒤, 그 remedy에 맞는 defendant priority gate를 적용한다.
          </remedy_specific_defendant_priority_rules>

          <historical_actor_fixation_guardrails>
          - `historical actor fixation` 또는 `defendant fixation error`를 금지한다.
          - 이는 현재 점유자·현재 사용자·현재 수익귀속자가 뒤에 존재하는데도, chain을 과거 신축자·과거 매도인·과거 임대인·과거 처분행위자에서 끊어 그 사람만 피고로 고정하는 오류를 뜻한다.
          - 인도·철거·부당이득 계열에서는 현재 점유자·현재 사용자·현재 수익귀속자 anchor가 확인되면, 과거 행위자만을 피고로 확정하지 않는다.
          - 특히 같은 cluster에서 후행 current possession state가 있으면, earlier builder/seller/lessor는 explanatory cause fact일 수는 있어도 present-possession relief의 자동 피고는 아니다.
          </historical_actor_fixation_guardrails>

          <generalized_goal_lock_rules>
          - 공동 통지·공동 최고·공동 해제·공동 소멸청구·공동 항의·공동 상담·공동 소송 희망 fact는 notice/enforcement/case-management fact이지, 단독으로 plaintiff standing anchor가 아니다.
          - 따라서 현재 권리귀속, 현재 점유 제거, 현재 사용이익 반환, 현재 권리확인 계열 청구에서 원고를 공동 통지 발신자 전원, 공동 상담 참여자 전원, 공동 희망 표명자 전원으로 기계적으로 확장하지 말라.
          - standing anchor가 1인으로 수렴하면 그 1인을 원고로 우선 바인딩하고, 공동행동 fact만으로 더 넓은 공동원고 구조를 남기지 말라.
          - 어떤 원고 후보가 직접 current right holder/direct creditor/direct tenant/direct owner anchor를 가지고 있고 다른 후보는 가족관계·배우자 관계·공동행동 fact만 가진다면, 직접 anchor를 가진 후보를 우선하고 가족관계·공동행동만으로 다른 후보를 sole plaintiff 또는 joint plaintiff로 올리지 말라.
          - 하나의 cluster에 historical/transfer chain과 later present-state chain이 함께 있으면, 경매 매수·후행 담보 취득·신축·매매·임대차·혼합계약·영업양도 등은 원칙적으로 historical/transfer chain이고, later current possession/use/benefit state는 present-state chain으로 본다.
          - 따라서 later current possession/use/benefit anchor가 있으면 `건물인도 청구`, `토지인도 청구`, `건물철거 및 토지인도 청구`, `부당이득반환 청구` 계열 피고는 그 later current possessor/current user/current benefit holder를 우선 다시 바인딩한다.
          - later current-state holder가 식별되는데도 earlier builder/seller/lessor/transferor/auction purchaser/formation actor만을 present-possession relief의 피고로 남기는 것을 금지한다.
          - later current-state signal은 있으나 holder identity가 1인으로 수렴하지 않으면, historical actor를 자동 기본값으로 고정하지 말고 completeness 또는 review_flags로 유보하거나 identified_entry_gate에 따라 excluded_items로 보낸다.
          - 특히 current-state signal은 존재하지만 current possessor/current user/current benefit holder identity가 비어 있거나 복수로 경쟁할 때, 1 plaintiff x 1 defendant gate를 맞추기 위해 earlier builder/seller/lessor/transferor/auction purchaser를 자동 fallback defendant로 채택하지 말라.
          - mixed_contract_doc, valuation_contexts, ongoing possession/use state가 결합되면 적어도 아래를 분리 검토한다.
          1. 현재 권리회복 chain
          2. 현재 부담제거 chain
          3. 현재 점유·사용 제거 chain
          4. `PRESENT_USE_GAIN_SLICE`의 현재 사용이익 반환 chain
          5. `HISTORICAL_CONSIDERATION_GAIN_SLICE`의 과거 대가수령·실질 이익취득 반환 chain
          - 같은 목적물 cluster에서 present-possession relief chain과 historical consideration-gain chain이 서로 다른 피고를 향하면, 하나의 generalized chain으로 병합하지 말고 독립 chain으로 유지한다.
          </generalized_goal_lock_rules>

          <projection_gap_fallback_rules>
          - `validation_warnings`에 upstream semantic projection 결손이 나타나면, 그 결손을 무시한 채 현재상태 기반 청구를 completeness=null로 확정하지 말라.
          - 특히 `mixed contract evidence exists but contract_components were not projected` 또는 `valuation evidence exists but valuation_object was not projected` 경고가 있으면, `부당이득반환 청구`의 completeness=null 확정을 원칙적으로 금지한다.
          - `standing_context` 결손 경고가 있거나 current-state signal은 있으나 current holder identity field가 비어 있으면, current-right / delivery / demolition / present-use-gain relief의 원고·피고를 성급히 단정하지 말고 `PLAINTIFF_STANDING_REVIEW`, `DEFENDANT_MATCH_REVIEW`, `CHAIN_SPLIT_REVIEW` 또는 excluded_items 처리로 유보하라.
          - projection gap이 있는 cluster에서는 공동행동 fact나 historical actor를 보정용 기본값으로 사용해 결손을 메우지 말라.
          </projection_gap_fallback_rules>

          <cluster_sensitive_scope_rules>
          - cluster-sensitive rebinding rule, goal lock rule, time-slice split rule, standing narrowing rule은 cluster 명칭 또는 asset/property cluster 정보만으로 작동하지 않는다.
          - 위 규칙들은 해당 cluster 명칭과 그 규칙이 겨냥하는 대응 fact bundle이 함께 충족될 때만 적용한다.
          - 즉, cluster label만 같다는 이유로 다른 사건의 원고·피고·구제수단 구조를 기계적으로 가져오지 말라.
          </cluster_sensitive_scope_rules>

          <party_pair_atomicity_rules>
          - identified_claims의 각 항목은 정확히 1명의 원고와 정확히 1명의 피고만 가진다.
          - `plaintiffs`와 `defendants`는 기존 downstream 호환을 위해 배열 형식을 유지하되, 길이는 반드시 1이어야 한다.
          - 2명 이상의 원고 후보가 남으면 하나의 identified_claims 항목으로 유지하지 말고 plaintiff별로 분리한다.
          - 2명 이상의 피고 후보가 남으면 relief chain을 피고별로 분리한다.
          - 원고 또는 피고를 1명으로 줄이지 못하면 identified_claims에 올리지 말고 excluded_items 또는 completeness+review로 관리하되, 복수 배열 상태로 확정 출력하지 않는다.
          </party_pair_atomicity_rules>

          <plaintiff_standing_resolution_rules>
          - 가족관계, 배우자 관계, 공동 내방, 공동 항의, 공동 통지, 공동 소송 희망은 원고적격의 직접 근거가 아니다.
          - 상속포기 문서가 있으면 상속인 범위를 좁히는 자료로만 사용하고, 그 자체로 특정 원고의 권리주체성을 자동 확정하지 않는다.
          - 원고적격은 다음 우선순위로 본다.
          1. 현재 소유권·등기귀속·직접 계약당사자
          2. 직접 채권자·직접 임차인 등 구조화된 권리주체
          3. 상속·승계 문서에 의해 직접 이어지는 자
          4. 그래도 불명확하면 `PLAINTIFF_STANDING_REVIEW`
          - client_goal.parties.plaintiffs는 client universe일 뿐, 사건군별 원고적격 확정 근거가 아니다.
          </plaintiff_standing_resolution_rules>

          <benefit_recipient_and_time_split_rules>
          - 부당이득반환 chain은 현재 점유자/현재 사용자/현재 수익귀속자와 과거 처분대가 수령자/실질 이익취득자를 구별한다.
          - 같은 목적물 cluster라도 이익 귀속 주체가 시간대별로 바뀌면 time band를 분리하여 별도 chain 후보를 만든다.
          - 부당이득반환의 time-slice 분리(unjust enrichment time-slice)는 필수다. 최소한 아래 두 slice를 독립 검토한다.
          1. `PRESENT_USE_GAIN_SLICE` = 현재 점유·현재 사용·현재 운영·현재 수익귀속에서 발생하는 사용이익 반환 chain
          2. `HISTORICAL_CONSIDERATION_GAIN_SLICE` = 과거 매매대금·보증금·대물변제 상당액·변제수령·부담해소 등 실질 이익취득 반환 chain
          - 특히 아래는 원칙적으로 별도 스캔한다.
          1. 현재 토지·건물 점유자 또는 사용자
          2. 임대차보증금, 매매대금, 대물변제 상당액, 채무면탈 상당 이익을 취득한 자
          3. 후행 담보부담 해소 또는 변제수령으로 실질 이익을 얻은 자
          - mixed_contract_doc + valuation_doc가 결합된 경우, land lease component와 current possession/use state를 `PRESENT_USE_GAIN_SLICE`의 직접 anchor로, sale component와 price recipient은 `HISTORICAL_CONSIDERATION_GAIN_SLICE`의 직접 anchor로 병렬 검토한다.
          - `PRESENT_USE_GAIN_SLICE`의 피고는 current possessor/current user/current benefit holder를 우선하고, `HISTORICAL_CONSIDERATION_GAIN_SLICE`의 피고는 price/deposit/consideration recipient을 우선한다.
          - 부당이득반환 청구는 `현재 점유 사용이익 반환`과 `과거 대가수령/실질 이익취득 반환`을 하나로 합치지 않는다.
          - present use-gain chain과 historical consideration-gain chain 중 하나만 identified_entry_gate를 충족하더라도, 다른 slice를 같은 피고·같은 source_fact_ids로 억지 병합하지 않는다.
          </benefit_recipient_and_time_split_rules>


          <identified_entry_gate>
          - 어떤 chain이 identified_claims에 들어가려면 아래 4개 최소 진입요건을 모두 충족해야 한다.
          1. plaintiffs와 defendants에 넣을 당사자를 각각 1명씩 특정할 수 있다.
          2. claim_title을 짧은 명사형 본안 청구권으로 특정할 수 있다.
          3. source_fact_ids에 넣을 결정적 fact_id를 2개 이상 5개 이하로 연결할 수 있다.
          4. defendants에 남아 있는 모든 피고가 HARD_EXCLUDED_DEFENDANT가 아니다.
          - 1 plaintiff + 1 defendant + 1 core relief chain으로 분해되지 않으면 identified_entry_gate를 충족하지 못한다.
          - 공동 내방, 공동 통지, 공동 희망만으로 공동원고 배열을 허용하지 않는다.
          - 위 4개 중 하나라도 안 되면 그 chain은 identified_claims에 넣지 말고 excluded_items로 보낸다.
          - 위 4개가 모두 되면, 4문턱이 일부 미충족이거나 경계 불명확하더라도 identified_claims에 포함시킬 수 있다. 이 경우 completeness에 태그를 기록한다.
          </identified_entry_gate>

          <classification_rules>
          - 각 chain마다 아래 4문턱을 점검한다.
          1. 권리발생 사실이 있는가
          2. 원고가 그 권리의 주체인가
          3. 피고가 그 의무의 상대방 또는 현재 침해자인가
          4. 현재 행사 가능한 상태인가
          - 4개 모두 명확히 충족 + identified_entry_gate 충족: identified_claims에 넣고 completeness는 null로 둔다.
          - identified_entry_gate는 충족하지만 4문턱 중 1개 이상이 미충족, 불명확, 또는 만족/불만족의 경계가 불명확: identified_claims에 넣고 completeness에 태그를 기록한다.
          - 청구권이 아니라 쟁점/항변이거나, 본안 주문으로 특정되지 않거나, identified_entry_gate를 충족하지 못하면 excluded_items로 보낸다.
          - HARD_EXCLUDED_DEFENDANT만을 상대로 하는 청구는 청구 구조가 보여도 excluded_items로 보낸다.
          - completeness=null은 아래가 모두 명확할 때만 허용한다.
          1. 단일 원고
          2. 단일 피고
          3. 권리주체 명확
          4. 피고의 의무상대방 또는 침해자 지위 명확
          5. 현재 행사 가능성 명확
          - 원고적격 또는 피고역할이 불명확한 경우에는 completeness에 반드시 `PLAINTIFF_STANDING_REVIEW` 또는 `DEFENDANT_MATCH_REVIEW`를 붙인다.
          - 복수 원고 배열 또는 복수 피고 배열이 남아 있으면 completeness=null 금지.
          - 증거 강도 보정:
          - credibility=high: 원칙적으로 직접 근거로 본다.
          - credibility=medium: evidence_strength.has_direct_evidence 또는 evidence_strength.has_corroboration이 있으면 유지 가능하다.
          - credibility=low이고 직접 증거도 없으면 원칙적으로 review_flags에 `EVIDENCE_GAP`을 붙여 유지 여부를 보수적으로 판단한다.
          - 단, 증거가 약하다는 사정만으로 자동 제외하지 말고, 청구유형 특정 가능성과 fact_id 연결 가능성을 먼저 본다.
          - 단, 소제기 실효성 하드 제외는 증거 강도와 무관하게 우선 적용한다.
          - validation_warnings가 있거나 source_trace.legal_calculation_conflicts가 권리귀속·역할·금액에 영향을 주면 completeness=null 로 성급히 확정하지 말고 completeness 또는 review_flags로 관리하라.
          </classification_rules>

          <claim_title_and_mapping_rules>
          - claim_title은 다음 단계 분류기가 바로 읽을 수 있는 짧은 명사형으로 쓴다.
          - 가능한 경우 아래 표준형을 우선 사용한다.
          - `물품대금 청구`
          - `대여금 청구`
          - `대출금 청구`
          - `보증채무금 청구`
          - `구상금 청구`
          - `사해행위취소 청구`
          - `소유권이전등기말소 청구`
          - `소유권보존등기말소 청구`
          - `근저당권설정등기말소 청구`
          - `가등기말소 청구`
          - `지상권설정등기말소 청구`
          - `전세권설정등기말소 청구`
          - `대위에의한소유권이전등기말소 청구`
          - `토지일부말소등기 청구`
          - `폐쇄등기기록등기말소 청구`
          - `사위판결등기말소 청구`
          - `기타등기말소 청구`
          - `건물철거 및 토지인도 청구`
          - `건물인도 청구`
          - `토지인도 청구`
          - `부당이득반환 청구`
          - `임차권확인 청구`
          - `손해배상청구`
          - 금전채권 일반에서 object_spec 또는 legal_keywords에 물품, 자재, 납품, 도매, 물품대금이 직접 드러나면 generic한 `매매대금 청구`보다 `물품대금 청구`를 우선한다.
          - 현재 등기귀속 회복이 핵심이면 현재 등기명의인을 상대로 `소유권이전등기말소 청구`를 우선 검토한다.
          - 현재 후행 근저당권 또는 담보부담 제거가 핵심이면 현재 근저당권자 또는 후행 부담권자를 상대로 `근저당권설정등기말소 청구`를 우선 검토한다.
          - 현재 점유 제거가 핵심이면 현재 점유자·현재 사용자를 상대로 `건물인도 청구`, `토지인도 청구`, 또는 사실구조에 따라 `건물철거 및 토지인도 청구`를 검토한다.
          - 건물 존재가 현재 점유·사용 구조의 핵심이면 `건물철거 및 토지인도 청구`를 우선 검토한다.
          - 같은 cluster에서 defendant가 현재 점유 또는 사용을 통해 이익을 얻고 있고 plaintiff의 우월한 권리귀속이 보이면 `부당이득반환 청구`를 병렬 검토한다.
          - evidence_semantics.valuation_contexts에 차임시세 또는 사용이익 가치가 있고 ongoing possession/use state가 있으면 부당이득반환 chain을 강하게 검토한다.
          - 현재 임차권 또는 그 존속 여부가 핵심이면 generic 확인청구보다 `임차권확인 청구`를 우선한다.
          - 사해행위 구조가 식별되면 `사해행위취소 청구`를 우선 사용한다. 원상회복 방식이 불명확하면 review_flags에 `RESTORATION_METHOD_REVIEW`를 붙인다.
          - `말소등기청구`라는 generic 제목은 금지한다. 말소대상 등기객체를 특정할 수 있으면 반드시 subtype-specific title을 사용한다.
          - 등기말소 subtype은 source_fact_ids로 연결된 items를 확인하여 다음처럼 정한다.
          - `소유권이전`, `경매절차에서의 소유권 취득`, `등록명의 취득` -> `소유권이전등기말소 청구`
          - `소유권보존` -> `소유권보존등기말소 청구`
          - `근저당권설정`, `근저당`, `저당`, `채권최고액`, `claim_security_provider`, `encumbrance holder` -> `근저당권설정등기말소 청구`
          - `가등기` -> `가등기말소 청구`
          - `지상권설정` -> `지상권설정등기말소 청구`
          - `전세권설정` -> `전세권설정등기말소 청구`
          - `대위에 의한 소유권이전등기` -> `대위에의한소유권이전등기말소 청구`
          - `토지 일부에 관한 말소` -> `토지일부말소등기 청구`
          - `폐쇄한 등기기록` -> `폐쇄등기기록등기말소 청구`
          - `사위판결` -> `사위판결등기말소 청구`
          - 같은 목적물 cluster 안에 현재 소유권이전등기와 현재 근저당권설정등기가 함께 있으면, 피고 및 말소대상 등기객체별로 반드시 별도 chain으로 분리한다.

          - 같은 목적물 cluster에서 `부당이득반환 청구`가 성립하더라도 현재 사용이익 반환 chain과 과거 대가수령·실질 이익취득 반환 chain은 자동 병합하지 말고 피고 및 time band별로 별도 검토한다.
          - 손해배상은 정말 더 직접적인 권리회복·반환·제거·확인 title이 없을 때만 사용한다.
          </claim_title_and_mapping_rules>

          <source_fact_id_rules>
          - source_fact_ids는 각 청구권마다 가장 결정적인 fact_id만 2개 이상 5개 이하로 고른다.
          - source_fact_ids에는 claim_identification_view.json.items[*].fact_id에 존재하는 id만 넣는다.
          - source_fact_ids는 단순 시간순 나열이 아니라 아래 역할을 충족하도록 고른다.
          1. 현재 권리귀속 또는 피보전채권 anchor fact 1개 이상
          2. 현재 등기·현재 부담·현재 점유·현재 사용·현재 운영·현재 이익귀속 상태 anchor fact 1개 이상
          3. 현재 상태를 설명하는 원인사실 또는 처분사실 anchor fact 1개 이상
          - 물품대금·대여금·대출금 청구는 원칙적으로 `채권발생 fact + 최고/연체/잔존채무 또는 현재행사 가능성을 보여 주는 fact`를 포함한다.
          - 말소등기 계열은 원칙적으로 `[plaintiff 우월권리 fact] + [문제되는 이전/설정 fact] + [현재 등기 또는 부담 존속 fact]` 구조를 우선한다.
          - 인도/철거 계열은 원칙적으로 `[plaintiff 우월권리 fact] + [현재 점유/사용 fact] + [건물 또는 점유형성 원인 fact]` 구조를 우선한다.
          - 부당이득반환 계열은 원칙적으로 `[plaintiff 우월권리 fact] + [현재 점유/사용 fact] + [차임시세/사용이익 또는 이익귀속 fact]` 구조를 우선한다.
          - `PRESENT_USE_GAIN_SLICE`의 source_fact_ids에는 반드시 현재 점유·사용 fact와 valuation/use-gain fact를 1개 이상 포함한다.
          - `HISTORICAL_CONSIDERATION_GAIN_SLICE`의 source_fact_ids에는 반드시 과거 대가수령·보증금·매매대금·변제수령·부담해소를 직접 가리키는 fact를 1개 이상 포함한다.
          - `PRESENT_USE_GAIN_SLICE`와 `HISTORICAL_CONSIDERATION_GAIN_SLICE`는 source_fact_ids가 완전히 동일하면 안 된다. 두 slice는 각각 최소 1개 이상의 비공통 fact를 가져야 한다.
          - 공동 통지 fact는 부당이득 chain에서 notice/경과 fact로는 사용할 수 있으나, 단독 standing fact나 단독 defendant-binding fact로 사용하지 않는다.
          - 임차권확인 계열은 원칙적으로 `[임대차 성립 fact] + [현재 점유/운영 또는 대항력 관련 fact] + [상대방의 부인·해지·인도요구 fact]` 구조를 우선한다.
          - actio 계열은 원칙적으로 `[피보전채권 fact] + [재산처분 fact] + [수익자/후행부담/이익귀속 fact]` 구조를 우선한다.
          - cluster_context.related_fact_ids_same_asset_cluster 및 related_fact_ids_same_property_cluster를 후보 풀로 적극 사용하되, 실제로 해당 청구에 직접 연결된 fact만 고른다.
          - 각 피고를 직접 가리키는 fact를 적어도 1개 이상 포함하도록 점검한다.
          - 같은 목적물 cluster에서 현재 사용이익 반환 chain과 과거 대가수령·실질 이익취득 반환 chain을 모두 검토하는 경우, 두 chain의 source_fact_ids는 time band와 이익귀속 주체를 구분하도록 별도로 구성한다.
          </source_fact_id_rules>

          <completeness_and_field_rules>
          - completeness 필드는 identified_claims의 모든 항목에 반드시 포함한다.
          - completeness=null 이면 identified_entry_gate 충족, 단일 원고·단일 피고, 권리주체·피고역할·현재 행사 가능성이 모두 명확한 것이다.
          - completeness 태그는 아래 허용 목록에서만 선택한다.
          - PARTY_IDENTIFIED
          - CLAIM_TYPE_IDENTIFIED
          - CORE_FACT_IDS_LINKED
          - RIGHT_GENERATION_REVIEW
          - PLAINTIFF_STANDING_REVIEW
          - DEFENDANT_MATCH_REVIEW
          - CURRENT_ENFORCEABILITY_REVIEW
          - completeness가 null이 아닌 경우에는 PARTY_IDENTIFIED, CLAIM_TYPE_IDENTIFIED, CORE_FACT_IDS_LINKED를 원칙적으로 모두 포함한다.
          - completeness 태그 수는 3개 이상 7개 이하로 제한한다.
          - claim_statement는 반드시 1문장이다.
          - completeness=null 인 항목의 claim_statement 형식: `원고 X는 피고 Y를 상대로 Z 청구를 할 수 있다.`
          - completeness가 null이 아닌 항목의 claim_statement 형식: `원고 X는 피고 Y를 상대로 Z 청구를 주장 후보로 식별할 수 있다.`
          - claim_statement에는 금액, 상세 법리, 세부 원상회복 방식, 장식어를 넣지 말라.
          - review_flags는 0개 이상 3개 이하의 짧은 코드형 문자열만 사용한다.
          - 허용 review_flags:
          EVIDENCE_GAP, PARTY_ID_RECHECK, DEFENDANT_SCOPE_REVIEW, AMOUNT_RECHECK, CURRENT_ENFORCEABILITY_REVIEW, BAD_FAITH_REVIEW, RESTORATION_METHOD_REVIEW, LIMITATION_REVIEW, JOINDER_REVIEW, PRESERVATION_RECOMMENDED, ROLE_DIRECTION_REVIEW, CHAIN_SPLIT_REVIEW, STRUCTURED_FIELD_CONFLICT, REGISTRY_SUBTYPE_REVIEW, DAMAGE_SUBTYPE_REVIEW
          - source_trace.legal_calculation_conflicts가 있고 그 충돌이 금액·역할·채권귀속에 영향을 주면 `STRUCTURED_FIELD_CONFLICT` 또는 `AMOUNT_RECHECK`를 붙인다.
          - 역할 방향이 충돌하거나 actors 의존 추정이 필요한 경우 `ROLE_DIRECTION_REVIEW`를 붙인다.
          - 같은 목적물 cluster에서 chain 분리가 불충분할 가능성이 있으면 `CHAIN_SPLIT_REVIEW`를 붙일 수 있다.
          - excluded_items.reason 허용값:
          - 쟁점 또는 근거 부족
          - 본안 청구권이 아님
          - 소제기 실효성 없음
          - HARD_EXCLUDED_DEFENDANT
          - 당사자 특정 부족
          - 청구유형 특정 부족
          - 핵심 fact_id 연결 부족
          - excluded_items.item은 가능하면 `F-001,F-002 / 후보청구명` 형식의 짧은 문자열로 쓴다.
          </completeness_and_field_rules>

          <related_measures_rules>
          - related_measures에는 본안 청구 외 조치만 짧게 적는다.
          - 예: 보전처분 필요, 특정 피고에 대한 집행보전 필요, 피보전채권 보전 필요, 집행 실익 없음, 다른 피고에 청구 집중.
          </related_measures_rules>

          <output_contract>
          - 반드시 JSON 객체 하나만 출력한다.
          - 아래 키만, 아래 순서로 출력한다.
          1. identified_claims
          2. excluded_items
          3. related_measures
          - 각 배열이 비어도 반드시 빈 배열로 출력한다.
          - conditional_claims 키는 출력하지 말라.
          - identified_claims의 claim_id는 최종 정렬 뒤 `C-001`, `C-002`, ... 순서로 새로 부여한다.
          - `plaintiffs`, `defendants`는 배열 형식을 유지하되, 각 항목의 길이는 반드시 1이어야 한다.
          </output_contract>

          <output_schema>
          {
          "identified_claims": [
              {
              "claim_id": "C-001",
              "claim_title": "string",
              "plaintiffs": ["원고A"],
              "defendants": ["피고A"],
              "claim_statement": "원고 X는 피고 Y를 상대로 Z 청구를 할 수 있다.",
              "source_fact_ids": ["F-001", "F-002"],
              "review_flags": ["AMOUNT_RECHECK"],
              "completeness": null
              }
          ],
          "excluded_items": [
              {
              "item": "F-010,F-011 / 후보청구명",
              "reason": "소제기 실효성 없음"
              }
          ],
          "related_measures": [
              "보전 필요 조치가 있으면 짧게 기재"
          ]
          }

          - `plaintiffs`, `defendants`는 배열이지만 각 길이는 반드시 1이어야 한다.
          - 길이를 1로 줄이지 못하면 identified_claims에 확정 출력하지 않는다.
          </output_schema>

          <sorting_rules>
          - identified_claims는 권리발생 시점이 빠른 순으로 정렬한다.
          - 권리발생 시점은 가능한 한 structured date를 우선 사용한다: claim_arising_date -> actio_pauliana_support.fraudulent_act_date -> claim_performance_date -> event_date.
          - 같은 시점이면 client_meeting.md와 client_goal.json.issue_clusters에서 드러나는 소송 우선순위가 높은 순으로 정렬한다.
          - completeness=null 인 항목을 completeness가 null이 아닌 항목보다 먼저 둔다. 다만 권리발생 시점 정렬이 우선이다.
          - 같은 cluster 안에서는 현재 권리회복 -> 현재 부담제거 -> 현재 점유제거 -> 현재 이익반환 -> 확인청구 순으로 보조 정렬할 수 있다.
          - HARD_EXCLUDED_DEFENDANT 관련 excluded_items는 excluded_items 배열의 앞부분에 우선 배치한다.
          </sorting_rules>

          <verification_loop>
          - finalizing 전 아래를 내부적으로 점검하라.
          - correctness: 각 identified_claims 항목이 정말 본안 청구권인지
          - grounding: 입력 파일에 없는 사실을 쓰지 않았는지
          - source discipline: client_meeting.md와 client_goal.json을 이용해 현재 권리귀속·현재 점유상태를 먼저 복원했는지
          - schema discipline: conditional_claims 키를 출력하지 않았는지
          - defendant viability discipline: HARD_EXCLUDED_DEFENDANT가 identified_claims.defendants에 남아 있지 않은지
          - atomicity discipline: identified_claims의 모든 항목이 정확히 1명의 원고와 1명의 피고만 갖는지
          - completeness discipline: completeness=null 인 항목은 단일 원고·단일 피고·권리주체 명확·피고역할 명확·현재 행사 가능성이 모두 충족되는지, completeness 배열이 있는 항목은 최소 진입요건 4개가 모두 충족되는지
          - tag discipline: completeness 태그가 허용 목록 안에만 있는지
          - exclusion discipline: 청구권 구조는 있으나 소제기 실효성 없는 피고에 대한 항목이 excluded_items로 빠졌는지
          - formatting: JSON만 출력하는지, 키 순서가 맞는지, claim_statement는 1문장인지, source_fact_ids는 2~5개인지, review_flags는 코드형인지
          - semantic coverage 1: 같은 property/asset cluster에 state_context 또는 linked_event_candidates.state가 있는데도 현재 점유자 상대 인도/철거/부당이득 청구가 전혀 검토되지 않았는지
          - semantic coverage 2: evidence_semantics.doc_semantic_flags에 `mixed_contract_doc`가 있고 contract_components가 2개 이상인데도 하나의 generic chain으로 뭉개지지 않았는지
          - semantic coverage 3: evidence_semantics.valuation_contexts가 있고 ongoing possession/use state가 있는데도 부당이득반환 청구가 누락되지 않았는지
          - semantic coverage 4: 같은 cluster에서 현재 등기명의인, 현재 근저당권자, 현재 점유자가 모두 다르면 말소등기 chain / 부담제거 chain / 인도·철거 chain / 부당이득 chain이 적어도 분리 검토되었는지
          - semantic coverage 5: 현재 점유자와 과거 행위자가 다를 때, delivery/demolition/unjust-enrichment 계열 피고가 과거 행위자로만 남아 있지 않은지
          - benefit split discipline: 현재 점유 사용이익 반환 chain과 과거 대가수령·실질 이익취득 반환 chain이 하나의 부당이득반환 청구로 뭉개지지 않았는지
          - human-lawyer standard: 과거 행위열이 아니라 `현재 권리귀속 -> 현재 침해상태 -> 현재 이익귀속 -> 구제수단` 순서로 재배치했는지
          - plaintiff narrowing: 통지 공동발송이나 공동상담만으로 원고가 과잉 확장되지 않았는지
          - plaintiff standing discipline: 가족관계·공동행동만으로 공동원고를 허용하지 않았는지, 불명확한 경우 `PLAINTIFF_STANDING_REVIEW`가 반영되었는지
          - defendant expansion: actors에 없더라도 현재 권리자·담보권자·점유자·수익귀속자가 피고 후보에서 누락되지 않았는지
          - remedy-specific defendant priority: 구제수단을 특정한 뒤 그 구제수단의 defendant priority gate가 실제로 적용되었는지
          - historical actor fixation guard: later current possessor/current user/current benefit holder가 있는데도 과거 신축자·과거 매도인·과거 임대인에서 chain을 끊지 않았는지
          - notice-vs-standing lock: 공동 통지·공동 최고·공동 행사 fact 때문에 직접 standing anchor보다 더 넓은 공동원고 구조나 잘못된 sole plaintiff가 남아 있지 않은지, standing anchor가 1인으로 수렴하면 그 1인으로 실제로 좁혀졌는지
          - standing non-convergence guard: tiers 1~4 standing anchor가 1인으로 수렴하지 않는데 tier 5 공동행동 fact만으로 sole plaintiff가 임의 선택되지 않았는지
          - projection-gap fallback: mixed-contract/valuation/standing projection 결손 경고가 있는 cluster에서 present-state relief 또는 unjust-enrichment relief가 completeness=null로 성급히 확정되지 않았는지
          - present-state rebinding lock: current possession/use/benefit anchor가 있는데도 인도·철거·부당이득 계열 피고가 earlier builder/seller/lessor/transfer actor/auction purchaser에 고정된 채 남아 있지 않은지
          - historical fallback guard: current-state signal은 있으나 holder identity가 비어 있거나 복수로 경쟁할 때 historical actor가 자동 fallback defendant로 사용되지 않았는지
          - unjust enrichment time-slice: `PRESENT_USE_GAIN_SLICE`와 `HISTORICAL_CONSIDERATION_GAIN_SLICE`가 피고·source_fact_ids·이익귀속 주체 측면에서 분리되었는지
          - unjust enrichment slice fact split: 두 slice의 source_fact_ids가 완전히 동일하지 않고, 각각 최소 1개 이상의 비공통 fact를 가지는지
          </verification_loop>
          </TASK_B_STATIC_BLOCK_V1_SLIM>

          <TASK_B_DYNAMIC_TAIL_V1_SLIM>
          <checklist>
          [] 1. Preflight: list_docs 사용하여 client_meeting.md, client_goal.json, claim_identification_view.json을 확인하고 read_docs 사용하여 client_meeting.md, client_goal.json, claim_identification_view.json을 읽는다.
          [] 2. write_file 사용하여 claims_identified.json 생성한다.
          [] 3. list_docs 사용하여 claims_identified.json 파일을 확인한다. 절대 claims_identified.json을 읽지(read) 않는다.
          [] 4. Terminate
          </checklist>

          <runtime_files>
          - 입력 파일:
          - client_meeting.md
          - client_goal.json
          - claim_identification_view.json
          - 출력 파일:
          - claims_identified.json
          </runtime_files>

          <final_instruction>
          - 분류와 검토는 내부적으로만 수행하고, 최종 답변에는 순수 JSON 텍스트만 출력하라.
          - 절대 마크다운 블록을 씌우지 말라.
          </final_instruction>
          </TASK_B_DYNAMIC_TAIL_V1_SLIM>

    - task_name: Task_C
      llm_provider: google
      llm_model: gemini-3.1-pro-preview
      llm_reasoning: medium
      llm_verbosity: low
      llm_endpoint: responses
      llm_history_continuous: Task_B
      preflight: false
      use_tools:
        - localdocs
      prompts:
      - role: user
        content: |
          <COMMON_CACHE_PREFIX_V1_SLIM>
            당신은 대한민국 민사소송 실무를 지원하는 정밀 법률 파이프라인 LLM이다.
            당신의 모든 응답은 후속 자동 단계의 직접 입력이 되므로, 구조적 안정성, 근거 통제, 재현 가능성, 보수적 판단이 우선한다.

            <ABSOLUTE_SYSTEM_RULES>
            - You MUST execute every single instruction provided below without any omission.
            - DO NOT arbitrarily reduce, summarize, or skip any steps, reasoning processes, or outputs.
            - DO NOT use placeholders like "..." or "rest of the code/text". Provide the exhaustive and complete output.
            - Adhere strictly to these rules before processing the prompt.
            </ABSOLUTE_SYSTEM_RULES>

            <base_rules>
            - 입력 파일에 없는 사건 고유 사실, 당사자, 청구, 금액, 날짜, 문서명, 증거, 법률효과를 창안하지 말라.
            - 일반 법지식은 해석 보조로만 사용할 수 있다. 사건별 사실 존재 여부는 반드시 입력 파일에서만 찾는다.
            - 모호하면 추정으로 메우지 말고 completeness, review_flags, excluded_items를 사용하라.
            - 현재 단계에서 지정된 파일만 읽고, 지정된 순서와 필드 범위를 우선한다.
            - stage-specific block이 더 구체적인 입력 범위, 판단 기준, 출력 스키마, 정렬 규칙을 제시하면 그 규칙을 우선한다.
            </base_rules>

            <identifier_and_output_rules>
            - 최종 응답은 반드시 JSON 객체 하나만 반환한다.
            - JSON 외의 설명문, 서문, 해설, 마크다운, 코드펜스, 주석을 절대 출력하지 말라.
            - 허용되지 않은 새 키를 만들지 말라.
            - stage-specific block이 정한 키 이름, 키 순서, 배열 이름, 파일 이름을 그대로 지켜라.
            - upstream 식별자와 앵커는 복사할 때 원형을 유지한다. 특히 fact_id, source_fact_ids, bo_id, evidence_index는 바꾸지 말라.
            - 다만 최종 출력 claim_id는 stage-specific output contract에 따라 최종 정렬 후 새로 부여할 수 있다.
            - 동일 입력이면 가능한 한 동일 출력이 나오도록 안정적으로 판단하라.
            </identifier_and_output_rules>

            <focus_rules>
            - 현재 단계에서 필요한 새 판단과 최소 출력에만 집중하라.
            - 이전 단계 산출물을 장문으로 재서술하지 말라.
            - 후속 자동처리를 해치는 장식적 문장, 스키마 해설, 메타 코멘트를 금지한다.
            </focus_rules>
          </COMMON_CACHE_PREFIX_V1_SLIM>

          <TASK_C_STATIC_BLOCK_V6>
          <role>
          당신은 대한민국 민사소송 사건분류 실무를 지원하는 Gemini-3.1-Pro-Preview 기반 정밀 taxonomy classifier이다.
          임무는 Task_B에서 식별된 각 청구권에 case_kinds.md의 hierarchy를 절대 준수하여 가장 정확한 사건 종류를 부여하고, downstream 호환을 위해 claim_types 배열을 유지한 채 계층 메타를 additive하게 남기는 것이다.
          </role>

          <objective>
          - 최우선 목표는 Task_B에서 식별한 각 청구권에 올바른 사건 종류를 할당하는 것이다.
          - 분류는 항상 `소송 대분류 -> 분쟁 유형 -> 사건 종류` 순서로 수행한다.
          - case_kind를 확정할 수 있을 때만 claim_type으로 사용하고, 확정할 수 없을 때만 backoff를 허용한다.
          </objective>

          <practical_caption_priority_rules>
          - 사건 종류 분류에서는 추상적 실체법 성질보다 실무상 주된 청구취지와 소장 표제를 우선한다.
          - linked fact는 claim_title을 보정하는 자료이지, 토지인도 중심 청구를 자동으로 방해제거형으로 재명명하는 근거가 아니다.
          - 현재 점유 회복, 현재 반환, 현재 인도가 중심이면 `토지의 인도를 구하는 소` 또는 `건물의 명도(인도)의 소`를 우선한다.
          </practical_caption_priority_rules>

          <read_scope>
          - 1층(항상 사용)
            - claims_identified.json: identified_claims[*].claim_id, claim_title, claim_statement, source_fact_ids, defendants, review_flags, completeness
            - Default_Agent/case_kinds.md: 사건 종류 표 전체
          - 2층(1층으로 case_kind를 즉시 확정할 수 없거나 subtype 판정이 필요한 경우에만 사용)
            - claim_identification_view.json에서 source_fact_ids로 조인되는 items[*]의 fact_id, action_label, bo_action_text, fact_action_text, juristic_act.label, legal_keywords, object_spec, fact_object_spec, bo_object, claim_event_type, party_roles.claim_security_provider, property_transaction.asset_id, property_transaction.property_label_for_schedule, property_transaction.transaction_type, property_transaction.transaction_date, property_transaction.registration_date, property_transaction.registry_recorded_transfer_date
          - 3층(2층으로도 경쟁 후보가 남을 때만 사용)
            - claim_identification_view.json에서 source_fact_ids로 조인되는 items[*]의 bo_meta.bo_type, state_context.is_state_fact, state_context.is_ongoing, linked_event_candidates[*].event_or_state, linked_event_candidates[*].event_kind, linked_event_candidates[*].action_summary, evidence_semantics.contract_components, evidence_semantics.valuation_contexts, cluster_context.asset_cluster_id, cluster_context.property_cluster_ids
          - 다른 필드는 읽지 않는다.
          </read_scope>

          <use_only>
          - claim_identification_view.json.items[*].state_context
          - claim_identification_view.json.items[*].linked_event_candidates
          - claim_identification_view.json.items[*].cluster_context
          - claim_identification_view.json.items[*].evidence_semantics.contract_components
          - claim_identification_view.json.items[*].evidence_semantics.valuation_contexts
          - 이 필드들을 읽어야 `현재 점유 회복 중심인가 / 방해제거 중심인가 / 현재 사용이익 반환 중심인가`를 제대로 구분할 수 있다.
          </use_only>

          <decision_tree>
          0. 각 청구마다 source_fact_ids로 linked facts를 조인한다.
          1. practical-caption gate:
             - claim_title 또는 claim_statement가 점유/인도/철거 중심 relief를 직접 드러내면 `<practical_caption_priority_rules>`와 `<caption_tiebreak_rules>`를 먼저 적용한다.
          2. title exact map:
             - claim_title이 아래 표준화 표의 단일 항목과 직접 또는 준-exact하게 대응하고, review_flags가 subtype 재검토를 요구하지 않으며, linked facts가 더 좁은 subtype 또는 다른 사건 종류를 시사하지 않으면 즉시 case_kind를 확정한다.
          3. linked-fact narrowing:
             - claim_title이 generic, umbrella, compound, table exact-mismatch, 또는 title-fact 충돌 상태이면 linked facts로 macro_category -> dispute_type -> case_kind를 확정한다.
          4. statement tiebreak:
             - 1, 2, 3단계 후에도 복수 case_kind 후보가 경쟁할 때만 claim_statement를 사용한다.
          5. backoff:
             - case_kind 미확정 시 dispute_type으로, dispute_type도 미확정 시 macro_category로 backoff한다.
          </decision_tree>

          <title_exact_map>
          - `연대보증채무 이행청구` -> `보증채무금 청구`
          - `보증채무금 청구` -> `보증채무금 청구`
          - `구상금 청구` -> `구상금 청구`
          - `대여금 청구` -> `대여금 청구`
          - `매매대금 청구` -> `매매대금 청구`
          - `부당이득반환청구` -> `부당이득금・이득상환금 청구`
          - `사해행위취소청구` -> `사해행위취소 청구`
          - `사해행위취소 및 원상회복청구` -> `사해행위취소 청구`
          - `사해행위취소 및 가액배상청구` -> `사해행위취소 청구`
          - `채권존재확인청구` -> `채권존재확인 청구`
          - `채권부존재확인청구` -> `채권부존재확인 청구`
          - `채무부존재확인청구` -> `채무부존재확인 청구`
          - `임차권존재확인청구` -> `임차권 확인 청구`
          - `건물인도청구` -> `건물의 명도(인도)의 소`
          - `건물명도청구` -> `건물의 명도(인도)의 소`
          - `토지인도청구` -> `토지의 인도를 구하는 소`
          - `소유권이전등기말소청구` -> `말소등기(소유권이전등기말소) 청구`
          - `소유권보존등기말소청구` -> `말소등기(소유권보존등기말소) 청구`
          - `근저당권설정등기말소청구` -> `말소등기(근저당권설정등기말소) 청구`
          - `가등기말소청구` -> `말소등기(가등기말소) 청구`
          - `지상권설정등기말소청구` -> `말소등기(지상권설정등기말소) 청구`
          - `전세권설정등기말소청구` -> `말소등기(전세권설정등기말소) 청구`
          - `대위에의한소유권이전등기말소청구` -> `말소등기(대위에의한소유권이전등기말소) 청구`
          - `토지일부말소등기청구` -> `말소등기(토지의일부만에관하여말소등기) 청구`
          - `폐쇄등기기록등기말소청구` -> `말소등기(폐쇄한등기기록에기록된등기의말소) 청구`
          - `사위판결등기말소청구` -> `말소등기(사위(詐僞)판결에의한등기의말소) 청구`
          - `기타등기말소청구` -> `말소등기(기타등기에관한말소) 청구`
          - `손해배상(자)청구` -> `손해배상(자) 청구`
          - `손해배상(산)청구` -> `손해배상(산) 청구`
          - `손해배상(의)청구` -> `손해배상(의) 청구`
          - `손해배상(지)청구` -> `손해배상(지) 청구`
          - `손해배상(저)청구` -> `손해배상(저) 청구`
          - `손해배상(언)청구` -> `손해배상(언) 청구`
          - `손해배상(건)청구` -> `손해배상(건) 청구`
          - `손해배상(국)청구` -> `손해배상(국) 청구`
          - `손해배상(해)청구` -> `손해배상(해) 청구`
          - `손해배상(기)청구` -> `손해배상(기) 청구`
          </title_exact_map>

          <blocked_generic_titles>
          - `말소등기청구`
          - `손해배상청구`
          - `대출금 청구`
          - `퇴거청구`
          - `명도청구`
          - `인도청구`
          - `건물철거 및 토지인도청구`
          </blocked_generic_titles>

          <linked_fact_rules>
          - 등기말소 subtype 판정은 linked facts의 action_label, bo_action_text, fact_action_text, juristic_act.label, legal_keywords, object_spec/fact_object_spec/bo_object, claim_event_type, party_roles.claim_security_provider를 우선한다.
          - 아래 단서가 직접 또는 가장 가깝게 나타나면 해당 case_kind를 선택한다.
            - `소유권이전`, `소유권이전등기`, `낙찰`, `경매 매각`, `등록명의 취득` -> `말소등기(소유권이전등기말소) 청구`
            - `소유권보존`, `소유권보존등기` -> `말소등기(소유권보존등기말소) 청구`
            - `근저당권설정`, `근저당`, `저당`, `채권최고액`, `담보제공`, `claim_security_provider` -> `말소등기(근저당권설정등기말소) 청구`
            - `가등기` -> `말소등기(가등기말소) 청구`
            - `지상권설정`, `지상권` -> `말소등기(지상권설정등기말소) 청구`
            - `전세권설정`, `전세권` -> `말소등기(전세권설정등기말소) 청구`
            - `대위에 의한 소유권이전등기` -> `말소등기(대위에의한소유권이전등기말소) 청구`
            - `토지 일부에 관한 말소` -> `말소등기(토지의일부만에관하여말소등기) 청구`
            - `폐쇄한 등기기록` -> `말소등기(폐쇄한등기기록에기록된등기의말소) 청구`
            - `사위(詐僞)판결` -> `말소등기(사위(詐僞)판결에의한등기의말소) 청구`
            - subtype을 끝내 특정할 수 없을 때만 `말소등기(기타등기에관한말소) 청구`
          - 손해배상 subtype 판정은 linked facts의 action_label, bo_action_text, fact_action_text, juristic_act.label, legal_keywords, object_spec/fact_object_spec/bo_object, claim_event_type, claim_statement를 우선한다.
          - 아래 단서가 직접 또는 고도의 명백성으로 나타나면 해당 case_kind를 선택한다.
            - `교통사고`, `자동차사고`, `차량사고`, `운행`, `대인사고` -> `손해배상(자) 청구`
            - `산업재해`, `근로재해`, `업무상재해` -> `손해배상(산) 청구`
            - `의료사고`, `의료과오`, `진료과실`, `수술과실` -> `손해배상(의) 청구`
            - `특허`, `상표`, `디자인`, `실용신안`, `영업비밀`, `부정경쟁`, `지식재산` -> `손해배상(지) 청구`
            - `저작권`, `저작인접권` -> `손해배상(저) 청구`
            - `언론`, `기사`, `보도`, `정정보도`, `반론보도`, `명예훼손` -> `손해배상(언) 청구`
            - `건축`, `건설`, `공사하자`, `건물하자`, `하자보수` -> `손해배상(건) 청구`
            - `국가배상`, `공무원`, `공권력 행사`, `공공기관의 위법행위` -> `손해배상(국) 청구`
            - `해상`, `선박`, `해난`, `충돌`, `해상운송` -> `손해배상(해) 청구`
            - linked facts가 `손해배상(기)`를 직접 지시 -> `손해배상(기) 청구`
            - subtype을 끝내 특정할 수 없을 때만 `손해배상 청구`
          - `대출금 청구`는 fast title 확정을 금지한다. linked facts가 `소비대차`, `차용`, `대여`, `대출`을 직접 가리키면 `대여금 청구`를 우선 검토하고, 더 직접 대응하는 table 항목이 있으면 그 항목을 선택한다.
          - 점유/인도/철거 복합 청구는 먼저 주된 주문의 중심을 판단한다.
            - 토지의 반환·인도 또는 현재 점유회복이 주된 취지이면 `토지의 인도를 구하는 소`를 우선한다.
            - 건물의 명도·퇴거가 주된 취지이면 `건물의 명도(인도)의 소`를 우선한다.
            - 건물 신축·존치·부지 사용 사실만으로 `건물철거 및 토지인도청구`를 자동으로 `소유물방해제거·방해예방청구`로 전환하지 않는다.
            - `소유물방해제거·방해예방청구`는 방해제거·예방 그 자체가 독립적·주된 청구취지이고, 인도회복이 부수적이거나 직접 표제화되어 있지 않을 때에만 선택한다.
            - claim_title 또는 claim_statement에 `토지인도`가 직접 들어 있고 source_fact_ids가 현재 점유자·현재 사용자·토지 부지 사용 상태를 보여주면 `토지의 인도를 구하는 소`를 우선한다.
          - 같은 목적물 cluster에 복수 등기객체가 있어도 현재 claim의 source_fact_ids와 defendants에 직접 대응하는 등기객체 하나만 선택한다.
          </linked_fact_rules>

          <caption_tiebreak_rules>
          - `건물철거 및 토지인도청구`와 `소유물방해제거·방해예방청구`가 모두 가능해 보일 때는 아래 순서로 결정한다.
            1. claim_title에 직접 적힌 relief
            2. claim_statement의 주문 중심 표현
            3. defendant가 현재 점유자·현재 사용자로 특정되는지
            4. 인도 회복이 주된 실익인지, 방해예방이 주된 실익인지
          - 동률이면 `토지의 인도를 구하는 소`를 우선하고, `taxonomy_meta.reason`에 철거 요소가 병합되어 있음을 보충 기재한다.
          </caption_tiebreak_rules>

          <hierarchy_and_backoff>
          - macro_category, dispute_type, case_kind는 case_kinds.md의 hierarchy 값만 사용한다.
          - case_kind가 확정되면 claim_type은 case_kind만 쓴다.
          - case_kind가 비어 있고 dispute_type이 확정되면 claim_type은 `소송 대분류-분쟁 유형`으로 쓴다.
          - dispute_type도 비어 있으면 claim_type은 `소송 대분류`만 쓴다.
          - `형성의 소`처럼 table상 dispute_type 자체가 비어 있는 경우를 오분류로 취급하지 않는다.
          - mapping_basis 허용값은 `title_exact`, `title_normalized`, `linked_facts`, `statement_tiebreak`, `backoff_dispute_type`, `backoff_macro_category`만 사용한다.
          </hierarchy_and_backoff>

          <output_contract>
          - 출력은 반드시 JSON 객체 하나만 반환한다.
          - claim_types 배열은 기존 downstream 호환을 위해 반드시 유지한다.
          - taxonomy_meta와 validation_warnings는 additive field다.
          - 출력 스키마:
          {
            "claim_types": [
              {
                "claim_id": "C-001",
                "claim_type": "string"
              }
            ],
            "taxonomy_meta": [
              {
                "claim_id": "C-001",
                "macro_category": "string|null",
                "dispute_type": "string|null",
                "case_kind": "string|null",
                "is_backoff_applied": false,
                "backoff_level": "case_kind|dispute_type|macro_category|null",
                "mapping_basis": "title_exact|title_normalized|linked_facts|statement_tiebreak|backoff_dispute_type|backoff_macro_category",
                "reason": "짧은 근거"
              }
            ],
            "validation_warnings": []
          }
          - claim_types는 identified_claims의 모든 claim_id를 입력 순서 그대로 포함한다.
          - taxonomy_meta도 claim_types와 같은 순서로 1:1 대응한다.
          - claim_id는 입력값을 그대로 복사한다.
          </output_contract>

          <validation>
          - 모든 claim_id가 claim_types와 taxonomy_meta에 정확히 1회씩 존재하는지 점검한다.
          - 사건 종류 표에 없는 새 case_kind를 만들지 않는다. 단, backoff로 생성한 `소송 대분류-분쟁 유형` 또는 `소송 대분류`는 허용한다.
          - `말소등기 청구` generic output 금지.
          - title exact map과 linked facts가 충돌하면 linked facts를 우선하고 taxonomy_meta.reason에 그 사실을 짧게 남긴다.
          - `건물철거 및 토지인도청구` 또는 claim_title/claim_statement에 `토지인도`가 직접 들어 있는 청구에서 current possessor/current user/current land-use가 읽히는 경우, 이를 이유로 자동으로 `소유물방해제거·방해예방청구`로 바꾸지 않았는지 점검한다.
          - `<caption_tiebreak_rules>`를 적용한 경우 `taxonomy_meta.reason`에 caption priority 또는 tie-break 적용 사유가 드러나야 한다.
          - is_backoff_applied, backoff_level, claim_type 형식이 서로 일치하는지 점검한다.
          - 최종 출력은 JSON only다.
          </validation>

          <final_instruction>
          분류는 내부적으로만 수행하고, 최종 답변에는 순수 JSON 텍스트만 출력하라. 절대 마크다운 블록을 씌우지 말라.
          </final_instruction>
          </TASK_C_STATIC_BLOCK_V6>

          <TASK_C_DYNAMIC_TAIL_V6>
          <checklist>
          [] 1. Preflight: list_docs 사용하여 claims_identified.json, claim_identification_view.json, Default_Agent/case_kinds.md 확인하고 read_docs 사용하여 claims_identified.json, claim_identification_view.json, Default_Agent/case_kinds.md를 읽는다.
          [] 2. write_file 사용하여 claims_identified_case_type.json 생성한다.
          [] 3. list_docs 사용하여 claims_identified_case_type.json 파일을 확인한다. 절대 claims_identified_case_type.json을 읽지(read) 않는다.
          [] 4. Terminate
          </checklist>

          <runtime_files>
          - 입력 파일:
          - claims_identified.json
          - claim_identification_view.json
          - Default_Agent/case_kinds.md
          - 출력 파일:
          - claims_identified_case_type.json
          </runtime_files>

          <runtime_scope>
          - claims_identified.json에서는 identified_claims[*].claim_id, claim_title, claim_statement, source_fact_ids, defendants, review_flags, completeness를 사용한다.
          - claim_identification_view.json에서는 source_fact_ids로 조인되는 items[*]의 fact_id, action_label, bo_action_text, fact_action_text, juristic_act.label, legal_keywords, object_spec, fact_object_spec, bo_object, claim_event_type, party_roles.claim_security_provider, property_transaction.asset_id, property_transaction.property_label_for_schedule, property_transaction.transaction_type, property_transaction.transaction_date, property_transaction.registration_date, property_transaction.registry_recorded_transfer_date, bo_meta.bo_type, state_context.is_state_fact, state_context.is_ongoing, linked_event_candidates[*].event_or_state, linked_event_candidates[*].event_kind, linked_event_candidates[*].action_summary, evidence_semantics.contract_components, evidence_semantics.valuation_contexts, cluster_context.asset_cluster_id, cluster_context.property_cluster_ids를 필요할 때만 사용한다.
          - case_kinds.md에서는 `소송 대분류 -> 분쟁 유형 -> 사건 종류` 표를 사용한다.
          - identified_claims의 모든 claim_id를 claims_identified.json의 입력 순서 그대로 출력한다.
          </runtime_scope>
          </TASK_C_DYNAMIC_TAIL_V6>
